Files
btpxpress-frontend/app/(main)/stock/commandes/page.tsx

841 lines
33 KiB
TypeScript
Executable File

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { Card } from 'primereact/card';
import { Dialog } from 'primereact/dialog';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { InputNumber } from 'primereact/inputnumber';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { InputTextarea } from 'primereact/inputtextarea';
import { confirmDialog } from 'primereact/confirmdialog';
import { ConfirmDialog } from 'primereact/confirmdialog';
interface OrderItem {
id: string;
reference: string;
nom: string;
quantiteCommandee: number;
quantiteRecue: number;
prixUnitaire: number;
total: number;
unite: string;
}
interface Order {
id: string;
numero: string;
fournisseur: string;
dateCommande: Date;
dateLivraison?: Date;
statut: 'BROUILLON' | 'ENVOYEE' | 'CONFIRMEE' | 'PARTIELLE' | 'LIVREE' | 'ANNULEE';
priorite: 'NORMALE' | 'URGENTE' | 'CRITIQUE';
montantTotal: number;
items: OrderItem[];
commentaires: string;
adresseLivraison: string;
responsable: string;
}
const CommandesPage = () => {
const [orders, setOrders] = useState<Order[]>([]);
const [selectedOrders, setSelectedOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [orderDialog, setOrderDialog] = useState(false);
const [itemDialog, setItemDialog] = useState(false);
const [deleteOrderDialog, setDeleteOrderDialog] = useState(false);
const [order, setOrder] = useState<Order>({
id: '',
numero: '',
fournisseur: '',
dateCommande: new Date(),
statut: 'BROUILLON',
priorite: 'NORMALE',
montantTotal: 0,
items: [],
commentaires: '',
adresseLivraison: '',
responsable: ''
});
const [currentItem, setCurrentItem] = useState<OrderItem>({
id: '',
reference: '',
nom: '',
quantiteCommandee: 0,
quantiteRecue: 0,
prixUnitaire: 0,
total: 0,
unite: 'unité'
});
const [submitted, setSubmitted] = useState(false);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Order[]>>(null);
const fournisseurs = [
{ label: 'Lafarge', value: 'lafarge' },
{ label: 'Bosch', value: 'bosch' },
{ label: 'Protecta', value: 'protecta' },
{ label: 'Leroy Merlin', value: 'leroy-merlin' },
{ label: 'Castorama', value: 'castorama' }
];
const priorites = [
{ label: 'Normale', value: 'NORMALE' },
{ label: 'Urgente', value: 'URGENTE' },
{ label: 'Critique', value: 'CRITIQUE' }
];
const statuts = [
{ label: 'Brouillon', value: 'BROUILLON' },
{ label: 'Envoyée', value: 'ENVOYEE' },
{ label: 'Confirmée', value: 'CONFIRMEE' },
{ label: 'Partielle', value: 'PARTIELLE' },
{ label: 'Livrée', value: 'LIVREE' },
{ label: 'Annulée', value: 'ANNULEE' }
];
const unites = [
{ label: 'Unité', value: 'unité' },
{ label: 'Mètre', value: 'm' },
{ label: 'Mètre carré', value: 'm²' },
{ label: 'Kilogramme', value: 'kg' },
{ label: 'Litre', value: 'l' },
{ label: 'Sac', value: 'sac' },
{ label: 'Palette', value: 'palette' }
];
useEffect(() => {
loadOrders();
}, []);
useEffect(() => {
// Recalculer le total de la commande quand les items changent
const total = order.items.reduce((sum, item) => sum + item.total, 0);
setOrder(prev => ({ ...prev, montantTotal: total }));
}, [order.items]);
const loadOrders = async () => {
try {
setLoading(true);
// Données mockées
const mockOrders: Order[] = [
{
id: '1',
numero: 'CMD-2024-001',
fournisseur: 'lafarge',
dateCommande: new Date('2024-01-15'),
dateLivraison: new Date('2024-01-22'),
statut: 'LIVREE',
priorite: 'NORMALE',
montantTotal: 1275.00,
items: [
{
id: '1',
reference: 'CIM-001',
nom: 'Ciment Portland',
quantiteCommandee: 150,
quantiteRecue: 150,
prixUnitaire: 8.50,
total: 1275.00,
unite: 'sac'
}
],
commentaires: 'Livraison urgente pour chantier Dupont',
adresseLivraison: '123 Rue du Chantier, 75001 Paris',
responsable: 'Jean Martin'
},
{
id: '2',
numero: 'CMD-2024-002',
fournisseur: 'bosch',
dateCommande: new Date('2024-01-20'),
statut: 'CONFIRMEE',
priorite: 'URGENTE',
montantTotal: 600.00,
items: [
{
id: '2',
reference: 'OUT-002',
nom: 'Perceuse électrique',
quantiteCommandee: 5,
quantiteRecue: 0,
prixUnitaire: 120.00,
total: 600.00,
unite: 'unité'
}
],
commentaires: 'Renouvellement outillage équipe',
adresseLivraison: 'Entrepôt A, Zone Industrielle',
responsable: 'Marie Dubois'
},
{
id: '3',
numero: 'CMD-2024-003',
fournisseur: 'protecta',
dateCommande: new Date(),
statut: 'BROUILLON',
priorite: 'CRITIQUE',
montantTotal: 500.00,
items: [
{
id: '3',
reference: 'SEC-003',
nom: 'Casque de sécurité',
quantiteCommandee: 20,
quantiteRecue: 0,
prixUnitaire: 25.00,
total: 500.00,
unite: 'unité'
}
],
commentaires: 'Commande urgente EPI',
adresseLivraison: 'Bureau central',
responsable: 'Pierre Durand'
}
];
setOrders(mockOrders);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les commandes',
life: 3000
});
} finally {
setLoading(false);
}
};
const openNew = () => {
setOrder({
id: '',
numero: '',
fournisseur: '',
dateCommande: new Date(),
statut: 'BROUILLON',
priorite: 'NORMALE',
montantTotal: 0,
items: [],
commentaires: '',
adresseLivraison: '',
responsable: ''
});
setSubmitted(false);
setOrderDialog(true);
};
const hideDialog = () => {
setSubmitted(false);
setOrderDialog(false);
};
const hideItemDialog = () => {
setSubmitted(false);
setItemDialog(false);
};
const hideDeleteOrderDialog = () => {
setDeleteOrderDialog(false);
};
const saveOrder = () => {
setSubmitted(true);
if (order.fournisseur && order.responsable && order.items.length > 0) {
let updatedOrders = [...orders];
if (order.id) {
// Mise à jour
const index = orders.findIndex(o => o.id === order.id);
updatedOrders[index] = order;
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Commande mise à jour',
life: 3000
});
} else {
// Création
const newOrder = {
...order,
id: Date.now().toString(),
numero: `CMD-${new Date().getFullYear()}-${String(orders.length + 1).padStart(3, '0')}`
};
updatedOrders.push(newOrder);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Commande créée',
life: 3000
});
}
setOrders(updatedOrders);
setOrderDialog(false);
}
};
const editOrder = (order: Order) => {
setOrder({ ...order });
setOrderDialog(true);
};
const confirmDeleteOrder = (order: Order) => {
setOrder(order);
setDeleteOrderDialog(true);
};
const deleteOrder = () => {
const updatedOrders = orders.filter(o => o.id !== order.id);
setOrders(updatedOrders);
setDeleteOrderDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Commande supprimée',
life: 3000
});
};
const sendOrder = (order: Order) => {
const updatedOrders = orders.map(o =>
o.id === order.id
? { ...o, statut: 'ENVOYEE' as const }
: o
);
setOrders(updatedOrders);
toast.current?.show({
severity: 'info',
summary: 'Commande envoyée',
detail: `Commande ${order.numero} envoyée au fournisseur`,
life: 3000
});
};
const receiveOrder = (order: Order) => {
const updatedOrders = orders.map(o =>
o.id === order.id
? {
...o,
statut: 'LIVREE' as const,
dateLivraison: new Date(),
items: o.items.map(item => ({ ...item, quantiteRecue: item.quantiteCommandee }))
}
: o
);
setOrders(updatedOrders);
toast.current?.show({
severity: 'success',
summary: 'Commande réceptionnée',
detail: `Commande ${order.numero} réceptionnée`,
life: 3000
});
};
const addItem = () => {
setCurrentItem({
id: '',
reference: '',
nom: '',
quantiteCommandee: 0,
quantiteRecue: 0,
prixUnitaire: 0,
total: 0,
unite: 'unité'
});
setSubmitted(false);
setItemDialog(true);
};
const saveItem = () => {
setSubmitted(true);
if (currentItem.nom && currentItem.quantiteCommandee > 0 && currentItem.prixUnitaire > 0) {
const item = {
...currentItem,
id: currentItem.id || Date.now().toString(),
total: currentItem.quantiteCommandee * currentItem.prixUnitaire
};
let updatedItems = [...order.items];
if (currentItem.id) {
// Mise à jour
const index = order.items.findIndex(i => i.id === currentItem.id);
updatedItems[index] = item;
} else {
// Ajout
updatedItems.push(item);
}
setOrder({ ...order, items: updatedItems });
setItemDialog(false);
}
};
const editItem = (item: OrderItem) => {
setCurrentItem({ ...item });
setItemDialog(true);
};
const deleteItem = (itemId: string) => {
const updatedItems = order.items.filter(i => i.id !== itemId);
setOrder({ ...order, items: updatedItems });
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvelle Commande"
icon="pi pi-plus"
severity="success"
onClick={openNew}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
onClick={exportCSV}
/>
);
};
const actionBodyTemplate = (rowData: Order) => {
return (
<div className="flex gap-2">
<Button
icon="pi pi-pencil"
rounded
severity="success"
onClick={() => editOrder(rowData)}
tooltip="Modifier"
/>
{rowData.statut === 'BROUILLON' && (
<Button
icon="pi pi-send"
rounded
severity="info"
onClick={() => sendOrder(rowData)}
tooltip="Envoyer"
/>
)}
{(rowData.statut === 'CONFIRMEE' || rowData.statut === 'PARTIELLE') && (
<Button
icon="pi pi-check"
rounded
severity="success"
onClick={() => receiveOrder(rowData)}
tooltip="Réceptionner"
/>
)}
<Button
icon="pi pi-trash"
rounded
severity="warning"
onClick={() => confirmDeleteOrder(rowData)}
tooltip="Supprimer"
/>
</div>
);
};
const statusBodyTemplate = (rowData: Order) => {
let severity: "success" | "warning" | "danger" | "info" = 'info';
let label = rowData.statut;
switch (rowData.statut) {
case 'BROUILLON':
severity = 'info';
label = 'Brouillon';
break;
case 'ENVOYEE':
severity = 'warning';
label = 'Envoyée';
break;
case 'CONFIRMEE':
severity = 'success';
label = 'Confirmée';
break;
case 'PARTIELLE':
severity = 'warning';
label = 'Partielle';
break;
case 'LIVREE':
severity = 'success';
label = 'Livrée';
break;
case 'ANNULEE':
severity = 'danger';
label = 'Annulée';
break;
}
return <Tag value={label} severity={severity} />;
};
const prioriteBodyTemplate = (rowData: Order) => {
let severity: "success" | "warning" | "danger" | "info" = 'info';
let label = rowData.priorite;
switch (rowData.priorite) {
case 'NORMALE':
severity = 'info';
label = 'Normale';
break;
case 'URGENTE':
severity = 'warning';
label = 'Urgente';
break;
case 'CRITIQUE':
severity = 'danger';
label = 'Critique';
break;
}
return <Tag value={label} severity={severity} />;
};
const fournisseurBodyTemplate = (rowData: Order) => {
const fournisseur = fournisseurs.find(f => f.value === rowData.fournisseur);
return fournisseur ? fournisseur.label : rowData.fournisseur;
};
const montantBodyTemplate = (rowData: Order) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(rowData.montantTotal);
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Gestion des Commandes</h5>
<span className="block mt-2 md:mt-0 p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
</div>
);
const orderDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={hideDialog} />
<Button label="Sauvegarder" icon="pi pi-check" onClick={saveOrder} />
</div>
);
const itemDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={hideItemDialog} />
<Button label="Ajouter" icon="pi pi-check" onClick={saveItem} />
</div>
);
const deleteOrderDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Non" icon="pi pi-times" text onClick={hideDeleteOrderDialog} />
<Button label="Oui" icon="pi pi-check" onClick={deleteOrder} />
</div>
);
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<ConfirmDialog />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
<DataTable
ref={dt}
value={orders}
selection={selectedOrders}
onSelectionChange={(e) => setSelectedOrders(e.value)}
dataKey="id"
paginator
rows={10}
rowsPerPageOptions={[5, 10, 25]}
className="datatable-responsive"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} commandes"
globalFilter={globalFilter}
emptyMessage="Aucune commande trouvée."
header={header}
responsiveLayout="scroll"
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="numero" header="Numéro" sortable />
<Column field="fournisseur" header="Fournisseur" body={fournisseurBodyTemplate} sortable />
<Column field="dateCommande" header="Date commande" body={(rowData) => rowData.dateCommande.toLocaleDateString()} sortable />
<Column field="dateLivraison" header="Date livraison" body={(rowData) => rowData.dateLivraison?.toLocaleDateString() || '-'} sortable />
<Column field="statut" header="Statut" body={statusBodyTemplate} sortable />
<Column field="priorite" header="Priorité" body={prioriteBodyTemplate} sortable />
<Column field="montantTotal" header="Montant" body={montantBodyTemplate} sortable />
<Column field="responsable" header="Responsable" sortable />
<Column body={actionBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
</DataTable>
{/* Dialog commande */}
<Dialog
visible={orderDialog}
style={{ width: '80vw' }}
header="Détails de la commande"
modal
className="p-fluid"
footer={orderDialogFooter}
onHide={hideDialog}
>
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="fournisseur">Fournisseur *</label>
<Dropdown
id="fournisseur"
value={order.fournisseur}
options={fournisseurs}
onChange={(e) => setOrder({...order, fournisseur: e.value})}
placeholder="Sélectionnez un fournisseur"
required
className={submitted && !order.fournisseur ? 'p-invalid' : ''}
/>
{submitted && !order.fournisseur && <small className="p-invalid">Le fournisseur est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="responsable">Responsable *</label>
<InputText
id="responsable"
value={order.responsable}
onChange={(e) => setOrder({...order, responsable: e.target.value})}
required
className={submitted && !order.responsable ? 'p-invalid' : ''}
/>
{submitted && !order.responsable && <small className="p-invalid">Le responsable est requis.</small>}
</div>
<div className="field col-12 md:col-4">
<label htmlFor="dateCommande">Date commande</label>
<Calendar
id="dateCommande"
value={order.dateCommande}
onChange={(e) => setOrder({...order, dateCommande: e.value || new Date()})}
showIcon
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="priorite">Priorité</label>
<Dropdown
id="priorite"
value={order.priorite}
options={priorites}
onChange={(e) => setOrder({...order, priorite: e.value})}
/>
</div>
<div className="field col-12 md:col-4">
<label htmlFor="statut">Statut</label>
<Dropdown
id="statut"
value={order.statut}
options={statuts}
onChange={(e) => setOrder({...order, statut: e.value})}
/>
</div>
<div className="field col-12">
<label htmlFor="adresseLivraison">Adresse de livraison</label>
<InputTextarea
id="adresseLivraison"
value={order.adresseLivraison}
onChange={(e) => setOrder({...order, adresseLivraison: e.target.value})}
rows={2}
/>
</div>
<div className="field col-12">
<label htmlFor="commentaires">Commentaires</label>
<InputTextarea
id="commentaires"
value={order.commentaires}
onChange={(e) => setOrder({...order, commentaires: e.target.value})}
rows={3}
/>
</div>
{/* Articles commandés */}
<div className="field col-12">
<div className="flex justify-content-between align-items-center mb-3">
<h6>Articles commandés</h6>
<Button
label="Ajouter Article"
icon="pi pi-plus"
size="small"
onClick={addItem}
/>
</div>
<DataTable
value={order.items}
responsiveLayout="scroll"
emptyMessage="Aucun article ajouté"
>
<Column field="reference" header="Référence" />
<Column field="nom" header="Nom" />
<Column field="quantiteCommandee" header="Qté" />
<Column field="unite" header="Unité" />
<Column field="prixUnitaire" header="Prix unitaire" body={(rowData) => `${rowData.prixUnitaire.toFixed(2)}`} />
<Column field="total" header="Total" body={(rowData) => `${rowData.total.toFixed(2)}`} />
<Column body={(rowData) => (
<div className="flex gap-2">
<Button
icon="pi pi-pencil"
size="small"
onClick={() => editItem(rowData)}
/>
<Button
icon="pi pi-trash"
size="small"
severity="danger"
onClick={() => deleteItem(rowData.id)}
/>
</div>
)} />
</DataTable>
<div className="text-right mt-3">
<strong>Total commande: {order.montantTotal.toFixed(2)}</strong>
</div>
</div>
</div>
</Dialog>
{/* Dialog article */}
<Dialog
visible={itemDialog}
style={{ width: '450px' }}
header="Article de commande"
modal
className="p-fluid"
footer={itemDialogFooter}
onHide={hideItemDialog}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="itemReference">Référence</label>
<InputText
id="itemReference"
value={currentItem.reference}
onChange={(e) => setCurrentItem({...currentItem, reference: e.target.value})}
/>
</div>
<div className="field col-12">
<label htmlFor="itemNom">Nom *</label>
<InputText
id="itemNom"
value={currentItem.nom}
onChange={(e) => setCurrentItem({...currentItem, nom: e.target.value})}
required
className={submitted && !currentItem.nom ? 'p-invalid' : ''}
/>
{submitted && !currentItem.nom && <small className="p-invalid">Le nom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="itemQuantite">Quantité *</label>
<InputNumber
id="itemQuantite"
value={currentItem.quantiteCommandee}
onValueChange={(e) => setCurrentItem({...currentItem, quantiteCommandee: e.value || 0})}
min={1}
required
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="itemUnite">Unité</label>
<Dropdown
id="itemUnite"
value={currentItem.unite}
options={unites}
onChange={(e) => setCurrentItem({...currentItem, unite: e.value})}
/>
</div>
<div className="field col-12">
<label htmlFor="itemPrix">Prix unitaire () *</label>
<InputNumber
id="itemPrix"
value={currentItem.prixUnitaire}
onValueChange={(e) => setCurrentItem({...currentItem, prixUnitaire: e.value || 0})}
mode="currency"
currency="EUR"
locale="fr-FR"
min={0}
required
/>
</div>
<div className="field col-12">
<label>Total: {(currentItem.quantiteCommandee * currentItem.prixUnitaire).toFixed(2)}</label>
</div>
</div>
</Dialog>
{/* Dialog suppression */}
<Dialog
visible={deleteOrderDialog}
style={{ width: '450px' }}
header="Confirmer"
modal
footer={deleteOrderDialogFooter}
onHide={hideDeleteOrderDialog}
>
<div className="flex align-items-center justify-content-center">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{order && (
<span>
Êtes-vous sûr de vouloir supprimer la commande <b>{order.numero}</b> ?
</span>
)}
</div>
</Dialog>
</Card>
</div>
</div>
);
};
export default CommandesPage;