Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,676 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Card } from 'primereact/card';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { InputNumber } from 'primereact/inputnumber';
import { Toast } from 'primereact/toast';
import { Divider } from 'primereact/divider';
import { Badge } from 'primereact/badge';
import { ProgressBar } from 'primereact/progressbar';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { InputTextarea } from 'primereact/inputtextarea';
import { Knob } from 'primereact/knob';
import { Toolbar } from 'primereact/toolbar';
import { Checkbox } from 'primereact/checkbox';
import { Chart } from 'primereact/chart';
import {
ActionButtonGroup,
ViewButton,
EditButton,
DeleteButton,
ActionButton
} from '../../../../components/ui/ActionButton';
// Types pour l'inventaire
interface InventoryItem {
id: string;
reference: string;
nom: string;
categorie: string;
unite: string;
stockTheorique: number;
stockReel: number;
ecart: number;
valeurStock: number;
emplacement: string;
dateInventaire: Date;
auditeur: string;
statut: 'OK' | 'ECART' | 'MANQUANT' | 'EXCEDENT';
}
interface InventorySession {
id: string;
nom: string;
dateDebut: Date;
dateFin?: Date;
statut: 'PLANIFIE' | 'EN_COURS' | 'TERMINE';
emplacements: string[];
categories: string[];
auditeur: string;
commentaires: string;
}
/**
* Page Inventaire Matériel BTP Express
* Gestion complète stocks, maintenances et alertes matériel
*/
const InventairePage = () => {
const [inventoryItems, setInventoryItems] = useState<InventoryItem[]>([]);
const [sessions, setSessions] = useState<InventorySession[]>([]);
const [selectedItems, setSelectedItems] = useState<InventoryItem[]>([]);
const [loading, setLoading] = useState(true);
const [inventaireDialog, setInventaireDialog] = useState(false);
const [maintenanceDialog, setMaintenanceDialog] = useState(false);
const [alertesDialog, setAlertesDialog] = useState(false);
const [selectedMateriel, setSelectedMateriel] = useState<any>(null);
const [nouvelleQuantite, setNouvelleQuantite] = useState<number>(0);
const [commentaireInventaire, setCommentaireInventaire] = useState('');
const [dateMaintenance, setDateMaintenance] = useState<Date | null>(null);
const [typeMaintenance, setTypeMaintenance] = useState('');
const [descriptionMaintenance, setDescriptionMaintenance] = useState('');
const [alertes, setAlertes] = useState<any[]>([]);
const [metriques, setMetriques] = useState<any>({});
const [globalFilter, setGlobalFilter] = useState('');
const [sessionDialog, setSessionDialog] = useState(false);
const [inventoryDialog, setInventoryDialog] = useState(false);
const [selectedSession, setSelectedSession] = useState<InventorySession | null>(null);
const [showStats, setShowStats] = useState(false);
const [submitted, setSubmitted] = useState(false);
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<InventoryItem[]>>(null);
const [newSession, setNewSession] = useState<InventorySession>({
id: '',
nom: '',
dateDebut: new Date(),
statut: 'PLANIFIE',
emplacements: [],
categories: [],
auditeur: '',
commentaires: ''
});
const emplacements = [
{ label: 'Entrepôt A', value: 'entrepot-a' },
{ label: 'Entrepôt B', value: 'entrepot-b' },
{ label: 'Magasin', value: 'magasin' },
{ label: 'Chantier Mobile', value: 'chantier-mobile' },
{ label: 'Bureau', value: 'bureau' }
];
const categories = [
{ label: 'Matériaux', value: 'materiaux' },
{ label: 'Outillage', value: 'outillage' },
{ label: 'Équipement', value: 'equipement' },
{ label: 'Consommables', value: 'consommables' },
{ label: 'Sécurité', value: 'securite' }
];
useEffect(() => {
loadInventoryData();
}, []);
const loadInventoryData = async () => {
try {
setLoading(true);
// Données mockées
const mockInventoryItems: InventoryItem[] = [
{
id: '1',
reference: 'CIM-001',
nom: 'Ciment Portland',
categorie: 'materiaux',
unite: 'sac',
stockTheorique: 150,
stockReel: 148,
ecart: -2,
valeurStock: 1258.00,
emplacement: 'entrepot-a',
dateInventaire: new Date(),
auditeur: 'Jean Dupont',
statut: 'ECART'
},
{
id: '2',
reference: 'OUT-002',
nom: 'Perceuse électrique',
categorie: 'outillage',
unite: 'unité',
stockTheorique: 5,
stockReel: 5,
ecart: 0,
valeurStock: 600.00,
emplacement: 'magasin',
dateInventaire: new Date(),
auditeur: 'Marie Martin',
statut: 'OK'
},
{
id: '3',
reference: 'SEC-003',
nom: 'Casque de sécurité',
categorie: 'securite',
unite: 'unité',
stockTheorique: 8,
stockReel: 10,
ecart: 2,
valeurStock: 250.00,
emplacement: 'bureau',
dateInventaire: new Date(),
auditeur: 'Pierre Durand',
statut: 'EXCEDENT'
}
];
const mockSessions: InventorySession[] = [
{
id: '1',
nom: 'Inventaire Mensuel Janvier 2024',
dateDebut: new Date('2024-01-15'),
dateFin: new Date('2024-01-17'),
statut: 'TERMINE',
emplacements: ['entrepot-a', 'magasin'],
categories: ['materiaux', 'outillage'],
auditeur: 'Équipe Inventaire',
commentaires: 'Inventaire mensuel standard'
},
{
id: '2',
nom: 'Inventaire Sécurité',
dateDebut: new Date(),
statut: 'EN_COURS',
emplacements: ['bureau'],
categories: ['securite'],
auditeur: 'Pierre Durand',
commentaires: 'Contrôle EPI obligatoire'
}
];
setInventoryItems(mockInventoryItems);
setSessions(mockSessions);
} catch (error) {
console.error('Erreur lors du chargement:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données d\'inventaire',
life: 3000
});
} finally {
setLoading(false);
}
};
const openNewSession = () => {
setNewSession({
id: '',
nom: '',
dateDebut: new Date(),
statut: 'PLANIFIE',
emplacements: [],
categories: [],
auditeur: '',
commentaires: ''
});
setSubmitted(false);
setSessionDialog(true);
};
const saveSession = () => {
setSubmitted(true);
if (newSession.nom.trim() && newSession.auditeur.trim()) {
const session = {
...newSession,
id: Date.now().toString()
};
setSessions([...sessions, session]);
setSessionDialog(false);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Session d\'inventaire créée',
life: 3000
});
}
};
const startInventory = (session: InventorySession) => {
const updatedSessions = sessions.map(s =>
s.id === session.id
? { ...s, statut: 'EN_COURS' as const }
: s
);
setSessions(updatedSessions);
toast.current?.show({
severity: 'info',
summary: 'Inventaire démarré',
detail: `Session "${session.nom}" en cours`,
life: 3000
});
};
const finishInventory = (session: InventorySession) => {
const updatedSessions = sessions.map(s =>
s.id === session.id
? { ...s, statut: 'TERMINE' as const, dateFin: new Date() }
: s
);
setSessions(updatedSessions);
toast.current?.show({
severity: 'success',
summary: 'Inventaire terminé',
detail: `Session "${session.nom}" terminée`,
life: 3000
});
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const generateReport = () => {
const totalItems = inventoryItems.length;
const itemsWithIssues = inventoryItems.filter(item => item.statut !== 'OK').length;
const totalValue = inventoryItems.reduce((sum, item) => sum + item.valeurStock, 0);
toast.current?.show({
severity: 'info',
summary: 'Rapport généré',
detail: `${totalItems} articles, ${itemsWithIssues} écarts, Valeur: ${totalValue.toFixed(2)}`,
life: 5000
});
setShowStats(true);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Nouvelle Session"
icon="pi pi-plus"
severity="success"
onClick={openNewSession}
/>
<Button
label="Rapport"
icon="pi pi-chart-bar"
severity="info"
onClick={generateReport}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
onClick={exportCSV}
/>
);
};
const statusBodyTemplate = (rowData: InventoryItem) => {
let severity: "success" | "warning" | "danger" | "info" = 'success';
let label = rowData.statut;
switch (rowData.statut) {
case 'OK':
severity = 'success';
label = 'OK';
break;
case 'ECART':
severity = 'warning';
label = 'Écart';
break;
case 'MANQUANT':
severity = 'danger';
label = 'Manquant';
break;
case 'EXCEDENT':
severity = 'info';
label = 'Excédent';
break;
}
return <Tag value={label} severity={severity} />;
};
const ecartBodyTemplate = (rowData: InventoryItem) => {
const ecart = rowData.ecart;
const className = ecart === 0 ? '' : ecart > 0 ? 'text-green-600' : 'text-red-600';
return <span className={className}>{ecart > 0 ? '+' : ''}{ecart}</span>;
};
const valeurBodyTemplate = (rowData: InventoryItem) => {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(rowData.valeurStock);
};
const categorieBodyTemplate = (rowData: InventoryItem) => {
const categorie = categories.find(c => c.value === rowData.categorie);
return categorie ? categorie.label : rowData.categorie;
};
const emplacementBodyTemplate = (rowData: InventoryItem) => {
const emplacement = emplacements.find(e => e.value === rowData.emplacement);
return emplacement ? emplacement.label : rowData.emplacement;
};
const sessionActionTemplate = (rowData: InventorySession) => {
return (
<ActionButtonGroup>
{rowData.statut === 'PLANIFIE' && (
<ActionButton
icon="pi pi-play"
color="success"
tooltip="Démarrer"
onClick={() => startInventory(rowData)}
/>
)}
{rowData.statut === 'EN_COURS' && (
<ActionButton
icon="pi pi-check"
color="info"
tooltip="Terminer"
onClick={() => finishInventory(rowData)}
/>
)}
<ViewButton
tooltip="Voir détails"
onClick={() => setSelectedSession(rowData)}
/>
</ActionButtonGroup>
);
};
const sessionStatusTemplate = (rowData: InventorySession) => {
let severity: "success" | "warning" | "danger" | "info" = 'info';
let label = rowData.statut;
switch (rowData.statut) {
case 'PLANIFIE':
severity = 'info';
label = 'Planifié';
break;
case 'EN_COURS':
severity = 'warning';
label = 'En cours';
break;
case 'TERMINE':
severity = 'success';
label = 'Terminé';
break;
}
return <Tag value={label} severity={severity} />;
};
const header = (
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
<h5 className="m-0">Inventaire du Stock</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 sessionDialogFooter = (
<div className="flex justify-content-end gap-2">
<Button label="Annuler" icon="pi pi-times" text onClick={() => setSessionDialog(false)} />
<Button label="Créer" icon="pi pi-check" onClick={saveSession} />
</div>
);
// Statistiques pour le graphique
const getStatsData = () => {
const statsOK = inventoryItems.filter(item => item.statut === 'OK').length;
const statsEcart = inventoryItems.filter(item => item.statut === 'ECART').length;
const statsManquant = inventoryItems.filter(item => item.statut === 'MANQUANT').length;
const statsExcedent = inventoryItems.filter(item => item.statut === 'EXCEDENT').length;
return {
labels: ['OK', 'Écart', 'Manquant', 'Excédent'],
datasets: [
{
data: [statsOK, statsEcart, statsManquant, statsExcedent],
backgroundColor: ['#10B981', '#F59E0B', '#EF4444', '#3B82F6'],
hoverBackgroundColor: ['#059669', '#D97706', '#DC2626', '#2563EB']
}
]
};
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
{/* Sessions d'inventaire */}
<div className="mb-4">
<h6>Sessions d'inventaire</h6>
<DataTable
value={sessions}
responsiveLayout="scroll"
className="mb-4"
>
<Column field="nom" header="Nom de la session" />
<Column field="dateDebut" header="Date début" body={(rowData) => rowData.dateDebut.toLocaleDateString()} />
<Column field="dateFin" header="Date fin" body={(rowData) => rowData.dateFin?.toLocaleDateString() || '-'} />
<Column field="statut" header="Statut" body={sessionStatusTemplate} />
<Column field="auditeur" header="Auditeur" />
<Column body={sessionActionTemplate} headerStyle={{ minWidth: '8rem' }} />
</DataTable>
</div>
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
{showStats && (
<Card className="mb-4" title="Statistiques d'inventaire">
<div className="grid">
<div className="col-12 md:col-6">
<Chart type="doughnut" data={getStatsData()} />
</div>
<div className="col-12 md:col-6">
<div className="grid">
<div className="col-6">
<div className="text-center">
<div className="text-2xl font-semibold text-green-600">
{inventoryItems.filter(item => item.statut === 'OK').length}
</div>
<div className="text-sm">Articles OK</div>
</div>
</div>
<div className="col-6">
<div className="text-center">
<div className="text-2xl font-semibold text-orange-600">
{inventoryItems.filter(item => item.statut !== 'OK').length}
</div>
<div className="text-sm">Avec écarts</div>
</div>
</div>
<div className="col-6">
<div className="text-center">
<div className="text-2xl font-semibold text-blue-600">
{inventoryItems.reduce((sum, item) => sum + item.valeurStock, 0).toFixed(0)}€
</div>
<div className="text-sm">Valeur totale</div>
</div>
</div>
<div className="col-6">
<div className="text-center">
<div className="text-2xl font-semibold text-purple-600">
{Math.round((inventoryItems.filter(item => item.statut === 'OK').length / inventoryItems.length) * 100)}%
</div>
<div className="text-sm">Conformité</div>
</div>
</div>
</div>
</div>
</div>
</Card>
)}
<DataTable
ref={dt}
value={inventoryItems}
selection={selectedItems}
onSelectionChange={(e) => setSelectedItems(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} articles"
globalFilter={globalFilter}
emptyMessage="Aucun article trouvé."
header={header}
responsiveLayout="scroll"
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="reference" header="Référence" sortable />
<Column field="nom" header="Nom" sortable />
<Column field="categorie" header="Catégorie" body={categorieBodyTemplate} sortable />
<Column field="stockTheorique" header="Stock théorique" sortable />
<Column field="stockReel" header="Stock réel" sortable />
<Column field="ecart" header="Écart" body={ecartBodyTemplate} sortable />
<Column field="valeurStock" header="Valeur" body={valeurBodyTemplate} sortable />
<Column field="emplacement" header="Emplacement" body={emplacementBodyTemplate} sortable />
<Column field="statut" header="Statut" body={statusBodyTemplate} sortable />
<Column field="auditeur" header="Auditeur" sortable />
</DataTable>
{/* Dialog pour nouvelle session */}
<Dialog
visible={sessionDialog}
style={{ width: '600px' }}
header="Nouvelle session d'inventaire"
modal
className="p-fluid"
footer={sessionDialogFooter}
onHide={() => setSessionDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="nom">Nom de la session</label>
<InputText
id="nom"
value={newSession.nom}
onChange={(e) => setNewSession({...newSession, nom: e.target.value})}
required
className={submitted && !newSession.nom ? 'p-invalid' : ''}
/>
{submitted && !newSession.nom && <small className="p-invalid">Le nom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateDebut">Date de début</label>
<Calendar
id="dateDebut"
value={newSession.dateDebut}
onChange={(e) => setNewSession({...newSession, dateDebut: e.value || new Date()})}
showIcon
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="auditeur">Auditeur</label>
<InputText
id="auditeur"
value={newSession.auditeur}
onChange={(e) => setNewSession({...newSession, auditeur: e.target.value})}
required
className={submitted && !newSession.auditeur ? 'p-invalid' : ''}
/>
{submitted && !newSession.auditeur && <small className="p-invalid">L'auditeur est requis.</small>}
</div>
<div className="field col-12">
<label>Emplacements à inventorier</label>
<div className="grid">
{emplacements.map((emplacement) => (
<div key={emplacement.value} className="col-6">
<Checkbox
inputId={emplacement.value}
value={emplacement.value}
onChange={(e) => {
const selected = [...newSession.emplacements];
if (e.checked) {
selected.push(e.value);
} else {
const index = selected.indexOf(e.value);
if (index > -1) {
selected.splice(index, 1);
}
}
setNewSession({...newSession, emplacements: selected});
}}
checked={newSession.emplacements.includes(emplacement.value)}
/>
<label htmlFor={emplacement.value} className="ml-2">{emplacement.label}</label>
</div>
))}
</div>
</div>
<div className="field col-12">
<label>Catégories à inventorier</label>
<div className="grid">
{categories.map((categorie) => (
<div key={categorie.value} className="col-6">
<Checkbox
inputId={categorie.value}
value={categorie.value}
onChange={(e) => {
const selected = [...newSession.categories];
if (e.checked) {
selected.push(e.value);
} else {
const index = selected.indexOf(e.value);
if (index > -1) {
selected.splice(index, 1);
}
}
setNewSession({...newSession, categories: selected});
}}
checked={newSession.categories.includes(categorie.value)}
/>
<label htmlFor={categorie.value} className="ml-2">{categorie.label}</label>
</div>
))}
</div>
</div>
</div>
</Dialog>
</Card>
</div>
</div>
);
};
export default InventairePage;