Check out our free templates made with AI and polished to perfection in Windframe

Get now

a month ago

'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { Eye, Edit, Trash2, Package, Calendar, Plus, AlertTriangle } from 'lucide-react' import Table from '@/components/ui/Table' import Button from '@/components/ui/Button' import Modal, { ModalFooter } from '@/components/ui/Modal' import ArticleCard from './ArticleCard' import { ROUTES } from '@/shared/constants/routes' import { getArticleDisponibiliteInfo, getEtatPhysiqueInfo, ArticleDisponibilite, } from '@/shared/constants/statuses' import { ArticleFilters, useArticles } from '@/application/hooks/useArticles' import { Article, StatutDisponibilite, EtatPhysique } from '@/domain/entities/Article' import type { Column } from '@/components/ui/Table' interface ArticleListProps { searchQuery: string filters: Record<string, any> } // Convertir les filtres pour correspondre à notre interface ArticleFilters const convertFiltersToArticleFilters = (filters: Record<string, any>): ArticleFilters => { return { nom: filters.nom || undefined, categorie: filters.categorie || undefined, statutDisponibilite: filters.statutDisponibilite as StatutDisponibilite || undefined, raisonIndisponibilite: filters.raisonIndisponibilite || undefined, // ✅ SOLUTION : Vérifier que la valeur existe dans l'enum avant de la caster etatPhysique: filters.etatPhysique && Object.values(EtatPhysique).includes(filters.etatPhysique as EtatPhysique) ? filters.etatPhysique as EtatPhysique : undefined, prixMin: filters.prixMin ? parseFloat(filters.prixMin) : undefined, prixMax: filters.prixMax ? parseFloat(filters.prixMax) : undefined, quantiteMinimale: filters.quantiteMinimale ? parseInt(filters.quantiteMinimale) : undefined, quantiteMin: filters.quantiteMin ? parseInt(filters.quantiteMin) : undefined, quantiteMax: filters.quantiteMax ? parseInt(filters.quantiteMax) : undefined } } // TÂCHE 1 : Fonction pour obtenir les infos de disponibilité avec raison mise à jour const getAvailabilityWithReason = (article: Article) => { const statutInfo = getArticleDisponibiliteInfo(article.statutDisponibilite as ArticleDisponibilite) let raison = '' switch (article.statutDisponibilite) { case StatutDisponibilite.DISPONIBLE: raison = 'Prêt à être loué' break case StatutDisponibilite.INDISPONIBLE: raison = article.raisonIndisponibilite || 'Temporairement indisponible' break default: raison = 'Statut non défini' } return { ...statutInfo, raison } } export default function ArticleList({ searchQuery, filters }: ArticleListProps) { const router = useRouter() const [viewMode, setViewMode] = useState<'table' | 'grid'>('table') const [deleteModal, setDeleteModal] = useState<{ open: boolean; article: Article | null }>({ open: false, article: null }) // Utiliser le hook connecté const { articles, loading, error, totalCount, currentPage, searchArticles, deleteArticle, clearError } = useArticles(25) // Pagination const [pageSize] = useState(25) // Tri const [sortColumn, setSortColumn] = useState<string | null>('updatedAt') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc') // Effectuer la recherche quand les paramètres changent useEffect(() => { const articleFilters = convertFiltersToArticleFilters(filters) searchArticles(searchQuery, articleFilters) }, [searchQuery, filters, searchArticles]) // Colonnes du tableau const columns: Column<Article>[] = [ { key: 'nom', label: 'Article', sortable: true, render: (value, article) => ( <div className="flex items-center gap-3"> <div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center"> <Package className="h-5 w-5 text-gray-600" /> </div> <div> <p className="font-medium text-gray-900">{article.nom}</p> <p className="text-sm text-gray-500">{article.categorie}</p> {/* TÂCHE 4 : Affichage quantité */} {article.quantity !== undefined && ( <p className="text-xs text-blue-600">Stock: {article.quantity}</p> )} </div> </div> ) }, { key: 'prixLocation', label: 'Prix', sortable: true, align: 'right', render: (value) => ( <span className="font-medium">{value.toFixed(2)} €/jour</span> ) }, // TÂCHE 4 : Nouvelle colonne quantité { key: 'quantity', label: 'Stock', sortable: true, align: 'center', render: (value) => { const quantity = value || 0 let badgeStyle = '' if (quantity === 0) { badgeStyle = 'bg-red-100 text-red-800' } else if (quantity < 3) { badgeStyle = 'bg-orange-100 text-orange-800' } else { badgeStyle = 'bg-green-100 text-green-800' } return ( <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${badgeStyle}`}> {quantity} </span> ) } }, { key: 'statutDisponibilite', label: 'Disponibilité', sortable: true, render: (_, article) => { const statutInfo = getAvailabilityWithReason(article) return ( <div className="flex flex-col gap-1"> <span className="status-badge inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium" style={{ backgroundColor: statutInfo.bgColor, color: statutInfo.color }} title={statutInfo.raison} > {statutInfo.label} </span> {/* TÂCHE 1 : Affichage raison d'indisponibilité */} {article.statutDisponibilite === StatutDisponibilite.INDISPONIBLE && article.raisonIndisponibilite && ( <span className="inline-flex items-center text-xs text-red-600"> <AlertTriangle className="w-3 h-3 mr-1" /> {article.raisonIndisponibilite} </span> )} </div> ) } }, { key: 'etatPhysique', label: 'État', sortable: true, render: (value, article) => { // TÂCHE 1 : Utilisation de la fonction utilitaire const etatInfo = getEtatPhysiqueInfo(value as EtatPhysique) return ( <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium" style={{ backgroundColor: etatInfo.bgColor, color: etatInfo.color }} > {etatInfo.label} </span> ) } }, { key: 'updatedAt', label: 'Modifié', sortable: true, render: (value) => value ? new Date(value).toLocaleDateString('fr-FR') : '-' }, { key: 'actions', label: 'Actions', align: 'center', render: (_, article) => ( <div className="flex items-center gap-1"> <Button variant="ghost" size="sm" icon={Eye} onClick={() => router.push(`/articles/${article.id}`)} title="Voir les détails" /> <Button variant="ghost" size="sm" icon={Calendar} onClick={() => router.push(`/articles/${article.id}/planning`)} title="Voir le planning" /> <Button variant="ghost" size="sm" icon={Trash2} onClick={() => setDeleteModal({ open: true, article })} title="Supprimer" className="text-red-600 hover:text-red-700" /> </div> ) } ] // Gestion du tri const handleSort = (column: string, direction: 'asc' | 'desc') => { setSortColumn(column) setSortDirection(direction) // TODO: Implémenter le tri côté serveur } // Gestion de la suppression const handleDelete = async () => { if (!deleteModal.article?.id) return try { const success = await deleteArticle(deleteModal.article.id) if (success) { setDeleteModal({ open: false, article: null }) } } catch (error) { console.error('Erreur lors de la suppression:', error) } } // Gestion du clic sur une ligne const handleRowClick = (article: Article) => { if (article.id) { router.push(ROUTES.ARTICLES_DETAIL(article.id)) } } // Afficher l'erreur si elle existe if (error) { return ( <div className="bg-red-50 border border-red-200 rounded-lg p-4"> <div className="flex items-center justify-between"> <div> <h3 className="text-red-800 font-medium">Erreur de chargement</h3> <p className="text-red-600 text-sm mt-1">{error}</p> </div> <Button variant="ghost" size="sm" onClick={clearError} className="text-red-600" > Fermer </Button> </div> </div> ) } return ( <> <div className="space-y-4"> {/* Contrôles et mode d'affichage */} <div className="flex items-center justify-between"> <div className="flex items-center gap-4"> <p className="text-sm text-gray-600"> {totalCount} article(s) trouvé(s) </p> {/* TÂCHE 1 : Indicateur d'articles indisponibles */} {articles.some(a => a.statutDisponibilite === StatutDisponibilite.INDISPONIBLE) && ( <div className="flex items-center gap-1 text-xs text-orange-600"> <AlertTriangle className="w-3 h-3" /> {articles.filter(a => a.statutDisponibilite === StatutDisponibilite.INDISPONIBLE).length} indisponible(s) </div> )} {/* TÂCHE 4 : Indicateur stock faible */} {articles.some(a => (a.quantity || 0) < 3 && (a.quantity || 0) > 0) && ( <div className="flex items-center gap-1 text-xs text-orange-600"> <Package className="w-3 h-3" /> {articles.filter(a => (a.quantity || 0) < 3 && (a.quantity || 0) > 0).length} stock faible </div> )} </div> <div className="flex items-center gap-2"> <Button variant={viewMode === 'table' ? 'primary' : 'outline'} size="sm" onClick={() => setViewMode('table')} > Tableau </Button> <Button variant={viewMode === 'grid' ? 'primary' : 'outline'} size="sm" onClick={() => setViewMode('grid')} > Grille </Button> </div> </div> {/* Affichage en tableau */} {viewMode === 'table' && ( <Table data={articles} columns={columns} loading={loading} pagination={{ page: currentPage, pageSize, total: totalCount, onPageChange: (page) => { // TODO: Implémenter la pagination console.log('Change page:', page) }, onPageSizeChange: (size) => { console.log('Change page size:', size) } }} sorting={{ column: sortColumn, direction: sortDirection, onSort: handleSort }} emptyMessage="Aucun article trouvé avec ces critères" /> )} {/* Affichage en grille */} {viewMode === 'grid' && ( <> {loading ? ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> {Array.from({ length: 8 }).map((_, i) => ( <div key={i} className="bg-gray-100 rounded-lg h-80 animate-pulse" /> ))} </div> ) : ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> {articles.map((article) => ( <ArticleCard key={article.id} article={article} onDelete={() => setDeleteModal({ open: true, article })} showQuantity={true} /> ))} </div> )} </> )} {/* Message si aucun résultat */} {articles.length === 0 && !loading && ( <div className="text-center py-12"> <Package className="h-12 w-12 text-gray-400 mx-auto mb-4" /> <h3 className="text-lg font-medium text-gray-900 mb-2"> Aucun article trouvé </h3> <p className="text-gray-600 mb-6"> Aucun article ne correspond à vos critères de recherche. </p> <Button variant="primary" onClick={() => router.push(ROUTES.ARTICLES_NEW)} icon={Plus} > Ajouter un article </Button> </div> )} </div> {/* Modal de confirmation de suppression */} <Modal isOpen={deleteModal.open} onClose={() => setDeleteModal({ open: false, article: null })} title="Supprimer l'article" footer={ <> <ModalFooter.Cancel onCancel={() => setDeleteModal({ open: false, article: null })} /> <ModalFooter.Confirm onConfirm={handleDelete} confirmText="Supprimer" variant="danger" loading={loading} /> </> } > <div className="space-y-4"> <p className="text-gray-600"> Êtes-vous sûr de vouloir supprimer l'article <strong>{deleteModal.article?.nom}</strong> ? </p> {/* TÂCHE 1 : Information sur la raison d'indisponibilité */} {deleteModal.article?.statutDisponibilite === StatutDisponibilite.INDISPONIBLE && deleteModal.article?.raisonIndisponibilite && ( <div className="bg-orange-50 border border-orange-200 rounded-lg p-4"> <p className="text-sm text-orange-800"> <AlertTriangle className="w-4 h-4 inline mr-1" /> Cet article est actuellement indisponible : {deleteModal.article.raisonIndisponibilite} </p> </div> )} {/* TÂCHE 4 : Information sur le stock */} {deleteModal.article?.quantity !== undefined && deleteModal.article.quantity > 0 && ( <div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> <p className="text-sm text-blue-800"> <Package className="w-4 h-4 inline mr-1" /> Stock actuel : {deleteModal.article.quantity} unité(s) </p> </div> )} <div className="bg-red-50 border border-red-200 rounded-lg p-4"> <p className="text-sm text-red-800"> ⚠️ Cette action est irréversible. L'article sera définitivement supprimé de votre catalogue. </p> </div> </div> </Modal> </> ) } tu peux améliorer le design de ce composant

Fork

Windframe is an AI visual editor for rapidly building stunning web UIs & websites

Start building stunning web UIs & websites!

Build from scratch or select prebuilt tailwind templates