This commit is contained in:
lejay
2026-02-13 00:21:53 +01:00
parent 81d2412835
commit 6923c319a1
85 changed files with 17836 additions and 0 deletions
+269
View File
@@ -0,0 +1,269 @@
import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import {
fetchFormexMaterials,
fetchFormexCollections,
deleteFormexMaterial
} from '../api/client';
import MaterialModal from '../components/MaterialModal';
import styles from './Materials.module.css';
export default function Materials() {
const { isAdmin } = useAuth();
const navigate = useNavigate();
// Data state
const [materials, setMaterials] = useState([]);
const [collections, setCollections] = useState([]);
const [pagination, setPagination] = useState({ page: 1, limit: 25, total: 0, totalPages: 0 });
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// Filter state
const [search, setSearch] = useState('');
const [collection, setCollection] = useState('');
const [searchInput, setSearchInput] = useState('');
// Modal state
const [modalOpen, setModalOpen] = useState(false);
const [editingMaterial, setEditingMaterial] = useState(null);
// Delete confirmation
const [deletingId, setDeletingId] = useState(null);
const loadMaterials = useCallback(async (page = 1) => {
setLoading(true);
setError('');
try {
const result = await fetchFormexMaterials({ search, collection, page, limit: 25 });
setMaterials(result.materials);
setPagination(result.pagination);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [search, collection]);
useEffect(() => {
if (!isAdmin) {
navigate('/login?next=/materiaux', { replace: true });
return;
}
// Load collections for filter dropdown
fetchFormexCollections()
.then(setCollections)
.catch(() => {});
// Load materials
loadMaterials(1);
}, [isAdmin, navigate, loadMaterials]);
const handleSearch = (e) => {
e.preventDefault();
setSearch(searchInput);
};
const handleCollectionChange = (e) => {
setCollection(e.target.value);
};
// Reload when filters change
useEffect(() => {
loadMaterials(1);
}, [search, collection, loadMaterials]);
const handlePageChange = (newPage) => {
loadMaterials(newPage);
};
const handleCreate = () => {
setEditingMaterial(null);
setModalOpen(true);
};
const handleEdit = (material) => {
setEditingMaterial(material);
setModalOpen(true);
};
const handleModalClose = () => {
setModalOpen(false);
setEditingMaterial(null);
};
const handleModalSave = () => {
setModalOpen(false);
setEditingMaterial(null);
loadMaterials(pagination.page);
};
const handleDeleteClick = (id) => {
setDeletingId(id);
};
const handleDeleteConfirm = async () => {
if (!deletingId) return;
try {
await deleteFormexMaterial(deletingId);
setDeletingId(null);
loadMaterials(pagination.page);
} catch (err) {
setError(err.message);
setDeletingId(null);
}
};
const handleDeleteCancel = () => {
setDeletingId(null);
};
if (!isAdmin) return null;
return (
<div className={styles.page}>
<div className={styles.header}>
<h1>Matériaux Formex</h1>
<button type="button" onClick={handleCreate} className={styles.createBtn}>
+ Nouveau matériau
</button>
</div>
{/* Filters */}
<div className={styles.filters}>
<form onSubmit={handleSearch} className={styles.searchForm}>
<input
type="text"
placeholder="Rechercher par nom ou code…"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
className={styles.searchInput}
/>
<button type="submit" className={styles.searchBtn}>Rechercher</button>
</form>
<select
value={collection}
onChange={handleCollectionChange}
className={styles.collectionSelect}
>
<option value="">Toutes les collections</option>
{collections.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
{error && <p className={styles.error}>{error}</p>}
{loading ? (
<p className={styles.muted}>Chargement…</p>
) : (
<>
<div className={styles.tableWrap}>
<table className={styles.table}>
<thead>
<tr>
<th>Code</th>
<th>Nom</th>
<th>Collection</th>
<th>Dimensions (mm)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{materials.length === 0 ? (
<tr>
<td colSpan={5} className={styles.empty}>
Aucun matériau trouvé.
</td>
</tr>
) : (
materials.map((m) => (
<tr key={m.id}>
<td className={styles.code}>{m.formex_code}</td>
<td>{m.name}</td>
<td>{m.collection}</td>
<td>
{m.width_mm && m.height_mm && m.thickness_mm
? `${m.width_mm} × ${m.height_mm} × ${m.thickness_mm}`
: '—'}
</td>
<td className={styles.actions}>
<button
type="button"
onClick={() => handleEdit(m)}
className={styles.editBtn}
>
Modifier
</button>
<button
type="button"
onClick={() => handleDeleteClick(m.id)}
className={styles.deleteBtn}
>
Supprimer
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className={styles.pagination}>
<button
type="button"
onClick={() => handlePageChange(pagination.page - 1)}
disabled={pagination.page <= 1}
className={styles.pageBtn}
>
Précédent
</button>
<span className={styles.pageInfo}>
Page {pagination.page} sur {pagination.totalPages} ({pagination.total} résultats)
</span>
<button
type="button"
onClick={() => handlePageChange(pagination.page + 1)}
disabled={pagination.page >= pagination.totalPages}
className={styles.pageBtn}
>
Suivant
</button>
</div>
)}
</>
)}
{/* Delete confirmation dialog */}
{deletingId && (
<div className={styles.overlay}>
<div className={styles.dialog}>
<p>Voulez-vous vraiment supprimer ce matériau ?</p>
<div className={styles.dialogActions}>
<button type="button" onClick={handleDeleteCancel} className={styles.cancelBtn}>
Annuler
</button>
<button type="button" onClick={handleDeleteConfirm} className={styles.confirmDeleteBtn}>
Supprimer
</button>
</div>
</div>
</div>
)}
{/* Create/Edit Modal */}
{modalOpen && (
<MaterialModal
material={editingMaterial}
collections={collections}
onClose={handleModalClose}
onSave={handleModalSave}
/>
)}
</div>
);
}