v1.0
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user