1025 lines
41 KiB
TypeScript
1025 lines
41 KiB
TypeScript
'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 { AutoComplete } from 'primereact/autocomplete';
|
|
import { confirmDialog } from 'primereact/confirmdialog';
|
|
import { ConfirmDialog } from 'primereact/confirmdialog';
|
|
import { Chart } from 'primereact/chart';
|
|
import {
|
|
ActionButtonGroup,
|
|
ViewButton,
|
|
EditButton,
|
|
DeleteButton,
|
|
ActionButton
|
|
} from '../../../../components/ui/ActionButton';
|
|
|
|
interface StockItem {
|
|
id: string;
|
|
reference: string;
|
|
nom: string;
|
|
stockDisponible: number;
|
|
unite: string;
|
|
emplacement: string;
|
|
}
|
|
|
|
interface MaterialExit {
|
|
id: string;
|
|
numero: string;
|
|
dateCreation: Date;
|
|
dateSortie: Date;
|
|
chantier: string;
|
|
responsable: string;
|
|
statut: 'BROUILLON' | 'VALIDEE' | 'SORTIE' | 'RETOURNEE';
|
|
motif: string;
|
|
articles: ExitItem[];
|
|
commentaires: string;
|
|
signature?: string;
|
|
dateRetourPrevue?: Date;
|
|
dateRetourEffective?: Date;
|
|
}
|
|
|
|
interface ExitItem {
|
|
id: string;
|
|
articleId: string;
|
|
reference: string;
|
|
nom: string;
|
|
quantiteSortie: number;
|
|
quantiteRetournee: number;
|
|
unite: string;
|
|
etat: 'BON' | 'ABIME' | 'PERDU';
|
|
commentaire: string;
|
|
}
|
|
|
|
const SortiesPage = () => {
|
|
const [exits, setExits] = useState<MaterialExit[]>([]);
|
|
const [stockItems, setStockItems] = useState<StockItem[]>([]);
|
|
const [selectedExits, setSelectedExits] = useState<MaterialExit[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [exitDialog, setExitDialog] = useState(false);
|
|
const [itemDialog, setItemDialog] = useState(false);
|
|
const [returnDialog, setReturnDialog] = useState(false);
|
|
const [deleteExitDialog, setDeleteExitDialog] = useState(false);
|
|
const [exit, setExit] = useState<MaterialExit>({
|
|
id: '',
|
|
numero: '',
|
|
dateCreation: new Date(),
|
|
dateSortie: new Date(),
|
|
chantier: '',
|
|
responsable: '',
|
|
statut: 'BROUILLON',
|
|
motif: '',
|
|
articles: [],
|
|
commentaires: ''
|
|
});
|
|
const [currentItem, setCurrentItem] = useState<ExitItem>({
|
|
id: '',
|
|
articleId: '',
|
|
reference: '',
|
|
nom: '',
|
|
quantiteSortie: 0,
|
|
quantiteRetournee: 0,
|
|
unite: '',
|
|
etat: 'BON',
|
|
commentaire: ''
|
|
});
|
|
const [filteredStockItems, setFilteredStockItems] = useState<StockItem[]>([]);
|
|
const [submitted, setSubmitted] = useState(false);
|
|
const toast = useRef<Toast>(null);
|
|
const dt = useRef<DataTable<MaterialExit[]>>(null);
|
|
|
|
const chantiers = [
|
|
{ label: 'Chantier Dupont - Paris 15ème', value: 'chantier-dupont' },
|
|
{ label: 'Rénovation Mairie - Lyon', value: 'chantier-mairie-lyon' },
|
|
{ label: 'Construction Villa - Marseille', value: 'chantier-villa-marseille' },
|
|
{ label: 'Bureau Central - Maintenance', value: 'bureau-maintenance' }
|
|
];
|
|
|
|
const responsables = [
|
|
{ label: 'Jean Martin', value: 'jean.martin' },
|
|
{ label: 'Marie Dubois', value: 'marie.dubois' },
|
|
{ label: 'Pierre Durand', value: 'pierre.durand' },
|
|
{ label: 'Sophie Bernard', value: 'sophie.bernard' }
|
|
];
|
|
|
|
const motifs = [
|
|
{ label: 'Utilisation chantier', value: 'utilisation-chantier' },
|
|
{ label: 'Prêt temporaire', value: 'pret-temporaire' },
|
|
{ label: 'Maintenance', value: 'maintenance' },
|
|
{ label: 'Formation', value: 'formation' },
|
|
{ label: 'Démonstration', value: 'demonstration' }
|
|
];
|
|
|
|
const statuts = [
|
|
{ label: 'Brouillon', value: 'BROUILLON' },
|
|
{ label: 'Validée', value: 'VALIDEE' },
|
|
{ label: 'Sortie', value: 'SORTIE' },
|
|
{ label: 'Retournée', value: 'RETOURNEE' }
|
|
];
|
|
|
|
const etats = [
|
|
{ label: 'Bon état', value: 'BON' },
|
|
{ label: 'Abîmé', value: 'ABIME' },
|
|
{ label: 'Perdu', value: 'PERDU' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Données mockées stock
|
|
const mockStockItems: StockItem[] = [
|
|
{
|
|
id: '1',
|
|
reference: 'CIM-001',
|
|
nom: 'Ciment Portland',
|
|
stockDisponible: 150,
|
|
unite: 'sac',
|
|
emplacement: 'entrepot-a'
|
|
},
|
|
{
|
|
id: '2',
|
|
reference: 'OUT-002',
|
|
nom: 'Perceuse électrique',
|
|
stockDisponible: 5,
|
|
unite: 'unité',
|
|
emplacement: 'magasin'
|
|
},
|
|
{
|
|
id: '3',
|
|
reference: 'SEC-003',
|
|
nom: 'Casque de sécurité',
|
|
stockDisponible: 8,
|
|
unite: 'unité',
|
|
emplacement: 'bureau'
|
|
},
|
|
{
|
|
id: '4',
|
|
reference: 'OUT-004',
|
|
nom: 'Visseuse sans fil',
|
|
stockDisponible: 3,
|
|
unite: 'unité',
|
|
emplacement: 'magasin'
|
|
},
|
|
{
|
|
id: '5',
|
|
reference: 'MAT-005',
|
|
nom: 'Échafaudage mobile',
|
|
stockDisponible: 2,
|
|
unite: 'unité',
|
|
emplacement: 'entrepot-b'
|
|
}
|
|
];
|
|
|
|
// Données mockées sorties
|
|
const mockExits: MaterialExit[] = [
|
|
{
|
|
id: '1',
|
|
numero: 'SOR-2024-001',
|
|
dateCreation: new Date('2024-01-15'),
|
|
dateSortie: new Date('2024-01-16'),
|
|
chantier: 'chantier-dupont',
|
|
responsable: 'jean.martin',
|
|
statut: 'SORTIE',
|
|
motif: 'utilisation-chantier',
|
|
articles: [
|
|
{
|
|
id: '1',
|
|
articleId: '2',
|
|
reference: 'OUT-002',
|
|
nom: 'Perceuse électrique',
|
|
quantiteSortie: 2,
|
|
quantiteRetournee: 0,
|
|
unite: 'unité',
|
|
etat: 'BON',
|
|
commentaire: ''
|
|
}
|
|
],
|
|
commentaires: 'Matériel pour travaux de perçage',
|
|
dateRetourPrevue: new Date('2024-01-30')
|
|
},
|
|
{
|
|
id: '2',
|
|
numero: 'SOR-2024-002',
|
|
dateCreation: new Date('2024-01-20'),
|
|
dateSortie: new Date('2024-01-20'),
|
|
chantier: 'chantier-mairie-lyon',
|
|
responsable: 'marie.dubois',
|
|
statut: 'RETOURNEE',
|
|
motif: 'utilisation-chantier',
|
|
articles: [
|
|
{
|
|
id: '2',
|
|
articleId: '3',
|
|
reference: 'SEC-003',
|
|
nom: 'Casque de sécurité',
|
|
quantiteSortie: 5,
|
|
quantiteRetournee: 4,
|
|
unite: 'unité',
|
|
etat: 'BON',
|
|
commentaire: '1 casque perdu'
|
|
}
|
|
],
|
|
commentaires: 'EPI pour équipe de 5 personnes',
|
|
dateRetourPrevue: new Date('2024-01-25'),
|
|
dateRetourEffective: new Date('2024-01-24')
|
|
},
|
|
{
|
|
id: '3',
|
|
numero: 'SOR-2024-003',
|
|
dateCreation: new Date(),
|
|
dateSortie: new Date(),
|
|
chantier: 'chantier-villa-marseille',
|
|
responsable: 'pierre.durand',
|
|
statut: 'BROUILLON',
|
|
motif: 'utilisation-chantier',
|
|
articles: [
|
|
{
|
|
id: '3',
|
|
articleId: '5',
|
|
reference: 'MAT-005',
|
|
nom: 'Échafaudage mobile',
|
|
quantiteSortie: 1,
|
|
quantiteRetournee: 0,
|
|
unite: 'unité',
|
|
etat: 'BON',
|
|
commentaire: ''
|
|
}
|
|
],
|
|
commentaires: 'Échafaudage pour travaux en hauteur',
|
|
dateRetourPrevue: new Date('2024-02-15')
|
|
}
|
|
];
|
|
|
|
setStockItems(mockStockItems);
|
|
setExits(mockExits);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les données',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const openNew = () => {
|
|
setExit({
|
|
id: '',
|
|
numero: '',
|
|
dateCreation: new Date(),
|
|
dateSortie: new Date(),
|
|
chantier: '',
|
|
responsable: '',
|
|
statut: 'BROUILLON',
|
|
motif: '',
|
|
articles: [],
|
|
commentaires: ''
|
|
});
|
|
setSubmitted(false);
|
|
setExitDialog(true);
|
|
};
|
|
|
|
const hideDialog = () => {
|
|
setSubmitted(false);
|
|
setExitDialog(false);
|
|
};
|
|
|
|
const hideItemDialog = () => {
|
|
setSubmitted(false);
|
|
setItemDialog(false);
|
|
};
|
|
|
|
const hideReturnDialog = () => {
|
|
setReturnDialog(false);
|
|
};
|
|
|
|
const hideDeleteExitDialog = () => {
|
|
setDeleteExitDialog(false);
|
|
};
|
|
|
|
const saveExit = () => {
|
|
setSubmitted(true);
|
|
|
|
if (exit.chantier && exit.responsable && exit.motif && exit.articles.length > 0) {
|
|
let updatedExits = [...exits];
|
|
|
|
if (exit.id) {
|
|
// Mise à jour
|
|
const index = exits.findIndex(e => e.id === exit.id);
|
|
updatedExits[index] = exit;
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Sortie mise à jour',
|
|
life: 3000
|
|
});
|
|
} else {
|
|
// Création
|
|
const newExit = {
|
|
...exit,
|
|
id: Date.now().toString(),
|
|
numero: `SOR-${new Date().getFullYear()}-${String(exits.length + 1).padStart(3, '0')}`
|
|
};
|
|
updatedExits.push(newExit);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Sortie créée',
|
|
life: 3000
|
|
});
|
|
}
|
|
|
|
setExits(updatedExits);
|
|
setExitDialog(false);
|
|
}
|
|
};
|
|
|
|
const editExit = (exit: MaterialExit) => {
|
|
setExit({ ...exit });
|
|
setExitDialog(true);
|
|
};
|
|
|
|
const confirmDeleteExit = (exit: MaterialExit) => {
|
|
setExit(exit);
|
|
setDeleteExitDialog(true);
|
|
};
|
|
|
|
const deleteExit = () => {
|
|
const updatedExits = exits.filter(e => e.id !== exit.id);
|
|
setExits(updatedExits);
|
|
setDeleteExitDialog(false);
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Succès',
|
|
detail: 'Sortie supprimée',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const validateExit = (exit: MaterialExit) => {
|
|
const updatedExits = exits.map(e =>
|
|
e.id === exit.id
|
|
? { ...e, statut: 'VALIDEE' as const }
|
|
: e
|
|
);
|
|
setExits(updatedExits);
|
|
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Sortie validée',
|
|
detail: `Sortie ${exit.numero} validée`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const processExit = (exit: MaterialExit) => {
|
|
const updatedExits = exits.map(e =>
|
|
e.id === exit.id
|
|
? { ...e, statut: 'SORTIE' as const, dateSortie: new Date() }
|
|
: e
|
|
);
|
|
setExits(updatedExits);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Sortie effectuée',
|
|
detail: `Matériel sorti pour ${exit.numero}`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const openReturn = (exit: MaterialExit) => {
|
|
setExit({ ...exit });
|
|
setReturnDialog(true);
|
|
};
|
|
|
|
const processReturn = () => {
|
|
const updatedExits = exits.map(e =>
|
|
e.id === exit.id
|
|
? { ...e, statut: 'RETOURNEE' as const, dateRetourEffective: new Date() }
|
|
: e
|
|
);
|
|
setExits(updatedExits);
|
|
setReturnDialog(false);
|
|
|
|
toast.current?.show({
|
|
severity: 'success',
|
|
summary: 'Retour traité',
|
|
detail: `Matériel retourné pour ${exit.numero}`,
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const addItem = () => {
|
|
setCurrentItem({
|
|
id: '',
|
|
articleId: '',
|
|
reference: '',
|
|
nom: '',
|
|
quantiteSortie: 0,
|
|
quantiteRetournee: 0,
|
|
unite: '',
|
|
etat: 'BON',
|
|
commentaire: ''
|
|
});
|
|
setSubmitted(false);
|
|
setItemDialog(true);
|
|
};
|
|
|
|
const saveItem = () => {
|
|
setSubmitted(true);
|
|
|
|
if (currentItem.articleId && currentItem.quantiteSortie > 0) {
|
|
const item = {
|
|
...currentItem,
|
|
id: currentItem.id || Date.now().toString()
|
|
};
|
|
|
|
let updatedItems = [...exit.articles];
|
|
|
|
if (currentItem.id) {
|
|
// Mise à jour
|
|
const index = exit.articles.findIndex(i => i.id === currentItem.id);
|
|
updatedItems[index] = item;
|
|
} else {
|
|
// Ajout
|
|
updatedItems.push(item);
|
|
}
|
|
|
|
setExit({ ...exit, articles: updatedItems });
|
|
setItemDialog(false);
|
|
}
|
|
};
|
|
|
|
const editItem = (item: ExitItem) => {
|
|
setCurrentItem({ ...item });
|
|
setItemDialog(true);
|
|
};
|
|
|
|
const deleteItem = (itemId: string) => {
|
|
const updatedItems = exit.articles.filter(i => i.id !== itemId);
|
|
setExit({ ...exit, articles: updatedItems });
|
|
};
|
|
|
|
const searchStockItems = (event: any) => {
|
|
const query = event.query.toLowerCase();
|
|
const filtered = stockItems.filter(item =>
|
|
item.nom.toLowerCase().includes(query) ||
|
|
item.reference.toLowerCase().includes(query)
|
|
);
|
|
setFilteredStockItems(filtered);
|
|
};
|
|
|
|
const onStockItemSelect = (item: StockItem) => {
|
|
setCurrentItem({
|
|
...currentItem,
|
|
articleId: item.id,
|
|
reference: item.reference,
|
|
nom: item.nom,
|
|
unite: item.unite
|
|
});
|
|
};
|
|
|
|
const exportCSV = () => {
|
|
dt.current?.exportCSV();
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
label="Nouvelle Sortie"
|
|
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: MaterialExit) => {
|
|
return (
|
|
<ActionButtonGroup>
|
|
<EditButton
|
|
onClick={() => editExit(rowData)}
|
|
tooltip="Modifier"
|
|
/>
|
|
{rowData.statut === 'BROUILLON' && (
|
|
<ActionButton
|
|
icon="pi pi-check"
|
|
color="info"
|
|
onClick={() => validateExit(rowData)}
|
|
tooltip="Valider"
|
|
/>
|
|
)}
|
|
{rowData.statut === 'VALIDEE' && (
|
|
<ActionButton
|
|
icon="pi pi-sign-out"
|
|
color="warning"
|
|
onClick={() => processExit(rowData)}
|
|
tooltip="Effectuer sortie"
|
|
/>
|
|
)}
|
|
{rowData.statut === 'SORTIE' && (
|
|
<ActionButton
|
|
icon="pi pi-sign-in"
|
|
color="help"
|
|
onClick={() => openReturn(rowData)}
|
|
tooltip="Retour"
|
|
/>
|
|
)}
|
|
<DeleteButton
|
|
onClick={() => confirmDeleteExit(rowData)}
|
|
tooltip="Supprimer"
|
|
/>
|
|
</ActionButtonGroup>
|
|
);
|
|
};
|
|
|
|
const statusBodyTemplate = (rowData: MaterialExit) => {
|
|
let severity: "success" | "warning" | "danger" | "info" = 'info';
|
|
let label = rowData.statut;
|
|
|
|
switch (rowData.statut) {
|
|
case 'BROUILLON':
|
|
severity = 'info';
|
|
label = 'Brouillon';
|
|
break;
|
|
case 'VALIDEE':
|
|
severity = 'warning';
|
|
label = 'Validée';
|
|
break;
|
|
case 'SORTIE':
|
|
severity = 'danger';
|
|
label = 'Sortie';
|
|
break;
|
|
case 'RETOURNEE':
|
|
severity = 'success';
|
|
label = 'Retournée';
|
|
break;
|
|
}
|
|
|
|
return <Tag value={label} severity={severity} />;
|
|
};
|
|
|
|
const chantierBodyTemplate = (rowData: MaterialExit) => {
|
|
const chantier = chantiers.find(c => c.value === rowData.chantier);
|
|
return chantier ? chantier.label : rowData.chantier;
|
|
};
|
|
|
|
const responsableBodyTemplate = (rowData: MaterialExit) => {
|
|
const responsable = responsables.find(r => r.value === rowData.responsable);
|
|
return responsable ? responsable.label : rowData.responsable;
|
|
};
|
|
|
|
const motifBodyTemplate = (rowData: MaterialExit) => {
|
|
const motif = motifs.find(m => m.value === rowData.motif);
|
|
return motif ? motif.label : rowData.motif;
|
|
};
|
|
|
|
const articlesBodyTemplate = (rowData: MaterialExit) => {
|
|
return `${rowData.articles.length} article(s)`;
|
|
};
|
|
|
|
const header = (
|
|
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
|
<h5 className="m-0">Sorties de Matériel</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 exitDialogFooter = (
|
|
<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={saveExit} />
|
|
</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 returnDialogFooter = (
|
|
<div className="flex justify-content-end gap-2">
|
|
<Button label="Annuler" icon="pi pi-times" text onClick={hideReturnDialog} />
|
|
<Button label="Traiter Retour" icon="pi pi-check" onClick={processReturn} />
|
|
</div>
|
|
);
|
|
|
|
const deleteExitDialogFooter = (
|
|
<div className="flex justify-content-end gap-2">
|
|
<Button label="Non" icon="pi pi-times" text onClick={hideDeleteExitDialog} />
|
|
<Button label="Oui" icon="pi pi-check" onClick={deleteExit} />
|
|
</div>
|
|
);
|
|
|
|
// Statistiques pour le graphique
|
|
const getExitStats = () => {
|
|
const brouillon = exits.filter(e => e.statut === 'BROUILLON').length;
|
|
const validee = exits.filter(e => e.statut === 'VALIDEE').length;
|
|
const sortie = exits.filter(e => e.statut === 'SORTIE').length;
|
|
const retournee = exits.filter(e => e.statut === 'RETOURNEE').length;
|
|
|
|
return {
|
|
labels: ['Brouillon', 'Validée', 'Sortie', 'Retournée'],
|
|
datasets: [
|
|
{
|
|
data: [brouillon, validee, sortie, retournee],
|
|
backgroundColor: ['#6366F1', '#F59E0B', '#EF4444', '#10B981'],
|
|
hoverBackgroundColor: ['#4F46E5', '#D97706', '#DC2626', '#059669']
|
|
}
|
|
]
|
|
};
|
|
};
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<Toast ref={toast} />
|
|
<ConfirmDialog />
|
|
|
|
{/* Statistiques */}
|
|
<div className="grid mb-4">
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-2xl font-semibold text-blue-600">
|
|
{exits.length}
|
|
</div>
|
|
<div className="text-sm">Total Sorties</div>
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-2xl font-semibold text-red-600">
|
|
{exits.filter(e => e.statut === 'SORTIE').length}
|
|
</div>
|
|
<div className="text-sm">En cours</div>
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-2xl font-semibold text-green-600">
|
|
{exits.filter(e => e.statut === 'RETOURNEE').length}
|
|
</div>
|
|
<div className="text-sm">Retournées</div>
|
|
</Card>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-2xl font-semibold text-orange-600">
|
|
{exits.filter(e => e.dateRetourPrevue && e.dateRetourPrevue < new Date() && e.statut === 'SORTIE').length}
|
|
</div>
|
|
<div className="text-sm">En retard</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
|
|
|
|
<DataTable
|
|
ref={dt}
|
|
value={exits}
|
|
selection={selectedExits}
|
|
onSelectionChange={(e) => setSelectedExits(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} sorties"
|
|
globalFilter={globalFilter}
|
|
emptyMessage="Aucune sortie trouvée."
|
|
header={header}
|
|
responsiveLayout="scroll"
|
|
loading={loading}
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
|
|
<Column field="numero" header="Numéro" sortable />
|
|
<Column field="dateCreation" header="Date création" body={(rowData) => rowData.dateCreation.toLocaleDateString()} sortable />
|
|
<Column field="chantier" header="Chantier" body={chantierBodyTemplate} sortable />
|
|
<Column field="responsable" header="Responsable" body={responsableBodyTemplate} sortable />
|
|
<Column field="motif" header="Motif" body={motifBodyTemplate} sortable />
|
|
<Column field="articles" header="Articles" body={articlesBodyTemplate} />
|
|
<Column field="dateRetourPrevue" header="Retour prévu" body={(rowData) => rowData.dateRetourPrevue?.toLocaleDateString() || '-'} sortable />
|
|
<Column field="statut" header="Statut" body={statusBodyTemplate} sortable />
|
|
<Column body={actionBodyTemplate} headerStyle={{ minWidth: '12rem' }} />
|
|
</DataTable>
|
|
|
|
{/* Dialog sortie */}
|
|
<Dialog
|
|
visible={exitDialog}
|
|
style={{ width: '80vw' }}
|
|
header="Détails de la sortie"
|
|
modal
|
|
className="p-fluid"
|
|
footer={exitDialogFooter}
|
|
onHide={hideDialog}
|
|
>
|
|
<div className="formgrid grid">
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="chantier">Chantier *</label>
|
|
<Dropdown
|
|
id="chantier"
|
|
value={exit.chantier}
|
|
options={chantiers}
|
|
onChange={(e) => setExit({...exit, chantier: e.value})}
|
|
placeholder="Sélectionnez un chantier"
|
|
required
|
|
className={submitted && !exit.chantier ? 'p-invalid' : ''}
|
|
/>
|
|
{submitted && !exit.chantier && <small className="p-invalid">Le chantier est requis.</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="responsable">Responsable *</label>
|
|
<Dropdown
|
|
id="responsable"
|
|
value={exit.responsable}
|
|
options={responsables}
|
|
onChange={(e) => setExit({...exit, responsable: e.value})}
|
|
placeholder="Sélectionnez un responsable"
|
|
required
|
|
className={submitted && !exit.responsable ? 'p-invalid' : ''}
|
|
/>
|
|
{submitted && !exit.responsable && <small className="p-invalid">Le responsable est requis.</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-4">
|
|
<label htmlFor="dateSortie">Date de sortie</label>
|
|
<Calendar
|
|
id="dateSortie"
|
|
value={exit.dateSortie}
|
|
onChange={(e) => setExit({...exit, dateSortie: e.value || new Date()})}
|
|
showIcon
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-4">
|
|
<label htmlFor="motif">Motif *</label>
|
|
<Dropdown
|
|
id="motif"
|
|
value={exit.motif}
|
|
options={motifs}
|
|
onChange={(e) => setExit({...exit, motif: e.value})}
|
|
placeholder="Sélectionnez un motif"
|
|
required
|
|
className={submitted && !exit.motif ? 'p-invalid' : ''}
|
|
/>
|
|
{submitted && !exit.motif && <small className="p-invalid">Le motif est requis.</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-4">
|
|
<label htmlFor="dateRetourPrevue">Date retour prévue</label>
|
|
<Calendar
|
|
id="dateRetourPrevue"
|
|
value={exit.dateRetourPrevue}
|
|
onChange={(e) => setExit({...exit, dateRetourPrevue: e.value || undefined})}
|
|
showIcon
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="commentaires">Commentaires</label>
|
|
<InputTextarea
|
|
id="commentaires"
|
|
value={exit.commentaires}
|
|
onChange={(e) => setExit({...exit, commentaires: e.target.value})}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
{/* Articles */}
|
|
<div className="field col-12">
|
|
<div className="flex justify-content-between align-items-center mb-3">
|
|
<h6>Articles à sortir</h6>
|
|
<Button
|
|
label="Ajouter Article"
|
|
icon="pi pi-plus"
|
|
size="small"
|
|
onClick={addItem}
|
|
/>
|
|
</div>
|
|
|
|
<DataTable
|
|
value={exit.articles}
|
|
responsiveLayout="scroll"
|
|
emptyMessage="Aucun article ajouté"
|
|
>
|
|
<Column field="reference" header="Référence" />
|
|
<Column field="nom" header="Nom" />
|
|
<Column field="quantiteSortie" header="Qté sortie" />
|
|
<Column field="unite" header="Unité" />
|
|
<Column field="etat" header="État" />
|
|
<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>
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Dialog article */}
|
|
<Dialog
|
|
visible={itemDialog}
|
|
style={{ width: '450px' }}
|
|
header="Article de sortie"
|
|
modal
|
|
className="p-fluid"
|
|
footer={itemDialogFooter}
|
|
onHide={hideItemDialog}
|
|
>
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<label htmlFor="article">Article *</label>
|
|
<AutoComplete
|
|
id="article"
|
|
value={currentItem.nom || ''}
|
|
suggestions={filteredStockItems}
|
|
completeMethod={searchStockItems}
|
|
field="nom"
|
|
placeholder="Rechercher un article..."
|
|
itemTemplate={(item) => `${item.reference} - ${item.nom} (Stock: ${item.stockDisponible})`}
|
|
onSelect={(e) => onStockItemSelect(e.value)}
|
|
onChange={(e) => setCurrentItem({...currentItem, nom: e.value})}
|
|
required
|
|
className={submitted && !currentItem.articleId ? 'p-invalid' : ''}
|
|
/>
|
|
{submitted && !currentItem.articleId && <small className="p-invalid">L'article est requis.</small>}
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="quantiteSortie">Quantité sortie *</label>
|
|
<InputNumber
|
|
id="quantiteSortie"
|
|
value={currentItem.quantiteSortie}
|
|
onValueChange={(e) => setCurrentItem({...currentItem, quantiteSortie: e.value || 0})}
|
|
min={1}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12 md:col-6">
|
|
<label htmlFor="etatSortie">État</label>
|
|
<Dropdown
|
|
id="etatSortie"
|
|
value={currentItem.etat}
|
|
options={etats}
|
|
onChange={(e) => setCurrentItem({...currentItem, etat: e.value})}
|
|
/>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<label htmlFor="commentaireItem">Commentaire</label>
|
|
<InputTextarea
|
|
id="commentaireItem"
|
|
value={currentItem.commentaire}
|
|
onChange={(e) => setCurrentItem({...currentItem, commentaire: e.target.value})}
|
|
rows={2}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Dialog retour */}
|
|
<Dialog
|
|
visible={returnDialog}
|
|
style={{ width: '600px' }}
|
|
header="Retour de matériel"
|
|
modal
|
|
className="p-fluid"
|
|
footer={returnDialogFooter}
|
|
onHide={hideReturnDialog}
|
|
>
|
|
<div className="formgrid grid">
|
|
<div className="field col-12">
|
|
<h6>Sortie: {exit.numero}</h6>
|
|
<p>Chantier: {chantierBodyTemplate(exit)}</p>
|
|
<p>Responsable: {responsableBodyTemplate(exit)}</p>
|
|
</div>
|
|
|
|
<div className="field col-12">
|
|
<h6>Articles à retourner</h6>
|
|
<DataTable
|
|
value={exit.articles}
|
|
responsiveLayout="scroll"
|
|
>
|
|
<Column field="reference" header="Référence" />
|
|
<Column field="nom" header="Nom" />
|
|
<Column field="quantiteSortie" header="Qté sortie" />
|
|
<Column field="quantiteRetournee" header="Qté retournée" body={(rowData) => (
|
|
<InputNumber
|
|
value={rowData.quantiteRetournee}
|
|
onValueChange={(e) => {
|
|
const updatedArticles = exit.articles.map(article =>
|
|
article.id === rowData.id
|
|
? { ...article, quantiteRetournee: e.value || 0 }
|
|
: article
|
|
);
|
|
setExit({ ...exit, articles: updatedArticles });
|
|
}}
|
|
min={0}
|
|
max={rowData.quantiteSortie}
|
|
/>
|
|
)} />
|
|
<Column field="etat" header="État" body={(rowData) => (
|
|
<Dropdown
|
|
value={rowData.etat}
|
|
options={etats}
|
|
onChange={(e) => {
|
|
const updatedArticles = exit.articles.map(article =>
|
|
article.id === rowData.id
|
|
? { ...article, etat: e.value }
|
|
: article
|
|
);
|
|
setExit({ ...exit, articles: updatedArticles });
|
|
}}
|
|
/>
|
|
)} />
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Dialog suppression */}
|
|
<Dialog
|
|
visible={deleteExitDialog}
|
|
style={{ width: '450px' }}
|
|
header="Confirmer"
|
|
modal
|
|
footer={deleteExitDialogFooter}
|
|
onHide={hideDeleteExitDialog}
|
|
>
|
|
<div className="flex align-items-center justify-content-center">
|
|
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
|
|
{exit && (
|
|
<span>
|
|
Êtes-vous sûr de vouloir supprimer la sortie <b>{exit.numero}</b> ?
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Dialog>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SortiesPage; |