719 lines
26 KiB
TypeScript
719 lines
26 KiB
TypeScript
// app/(app)/staff/gestion-productions/page.tsx
|
|
"use client";
|
|
|
|
import React, { useState, useMemo } from "react";
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
Loader2,
|
|
Search,
|
|
Filter,
|
|
X,
|
|
Plus,
|
|
Edit2,
|
|
Trash2,
|
|
Calendar,
|
|
Building2,
|
|
Hash,
|
|
FileText
|
|
} from "lucide-react";
|
|
import { usePageTitle } from "@/hooks/usePageTitle";
|
|
|
|
// Types
|
|
type Production = {
|
|
id: string;
|
|
name: string;
|
|
reference: string | null;
|
|
declaration_date: string | null;
|
|
org_id: string;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
};
|
|
|
|
type Organization = {
|
|
id: string;
|
|
name: string;
|
|
structure_api: string;
|
|
};
|
|
|
|
// Hook pour vérifier si l'utilisateur est staff
|
|
function useStaffCheck() {
|
|
return useQuery({
|
|
queryKey: ["staff-check"],
|
|
queryFn: async () => {
|
|
const res = await fetch("/api/me");
|
|
if (!res.ok) return { isStaff: false };
|
|
const data = await res.json();
|
|
return { isStaff: Boolean(data.is_staff || data.isStaff) };
|
|
},
|
|
staleTime: 30_000,
|
|
});
|
|
}
|
|
|
|
// Hook pour récupérer les organisations
|
|
function useOrganizations() {
|
|
return useQuery({
|
|
queryKey: ["organizations"],
|
|
queryFn: async () => {
|
|
const res = await fetch("/api/staff/organizations");
|
|
if (!res.ok) throw new Error("Erreur chargement organisations");
|
|
const data = await res.json();
|
|
return data.organizations as Organization[];
|
|
},
|
|
staleTime: 60_000,
|
|
});
|
|
}
|
|
|
|
// Hook pour récupérer les productions
|
|
function useProductions(orgId?: string, searchQuery?: string) {
|
|
return useQuery({
|
|
queryKey: ["staff-productions", orgId, searchQuery],
|
|
queryFn: async () => {
|
|
const params = new URLSearchParams();
|
|
if (orgId) params.set("org_id", orgId);
|
|
if (searchQuery) params.set("q", searchQuery);
|
|
|
|
const res = await fetch(`/api/staff/productions?${params.toString()}`);
|
|
if (!res.ok) throw new Error("Erreur chargement productions");
|
|
const data = await res.json();
|
|
return data.productions as Production[];
|
|
},
|
|
enabled: true,
|
|
staleTime: 15_000,
|
|
});
|
|
}
|
|
|
|
// Composant Modal pour Créer/Éditer
|
|
function ProductionModal({
|
|
isOpen,
|
|
onClose,
|
|
production,
|
|
organizations,
|
|
}: {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
production?: Production | null;
|
|
organizations: Organization[];
|
|
}) {
|
|
const queryClient = useQueryClient();
|
|
const [name, setName] = useState(production?.name || "");
|
|
const [reference, setReference] = useState(production?.reference || "");
|
|
const [declarationDate, setDeclarationDate] = useState(production?.declaration_date || "");
|
|
const [orgId, setOrgId] = useState(production?.org_id || "");
|
|
const [error, setError] = useState("");
|
|
|
|
React.useEffect(() => {
|
|
if (isOpen) {
|
|
setName(production?.name || "");
|
|
setReference(production?.reference || "");
|
|
setDeclarationDate(production?.declaration_date || "");
|
|
setOrgId(production?.org_id || "");
|
|
setError("");
|
|
}
|
|
}, [isOpen, production]);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: async (data: any) => {
|
|
const url = production
|
|
? `/api/staff/productions/${production.id}`
|
|
: "/api/staff/productions";
|
|
const method = production ? "PATCH" : "POST";
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errorData = await res.json();
|
|
throw new Error(errorData.error || "Erreur lors de l'opération");
|
|
}
|
|
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["staff-productions"] });
|
|
onClose();
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.message || "Une erreur est survenue");
|
|
},
|
|
});
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
|
|
if (!name.trim()) {
|
|
setError("Le nom est requis");
|
|
return;
|
|
}
|
|
|
|
if (!orgId) {
|
|
setError("L'organisation est requise");
|
|
return;
|
|
}
|
|
|
|
mutation.mutate({
|
|
name: name.trim(),
|
|
reference: reference.trim() || null,
|
|
declaration_date: declarationDate || null,
|
|
org_id: orgId,
|
|
});
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
{/* Backdrop */}
|
|
<div
|
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
|
|
{/* Modal */}
|
|
<div className="relative w-full max-w-2xl bg-white rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b bg-gradient-to-r from-indigo-50 to-purple-50">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-white rounded-lg shadow-sm">
|
|
<Building2 className="w-5 h-5 text-indigo-600" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-slate-900">
|
|
{production ? "Modifier la production" : "Nouvelle production"}
|
|
</h2>
|
|
<p className="text-sm text-slate-600">
|
|
{production ? "Modifiez les informations ci-dessous" : "Remplissez les informations ci-dessous"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-white/50 rounded-lg transition-colors"
|
|
>
|
|
<X className="w-5 h-5 text-slate-500" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
{error && (
|
|
<div className="p-4 bg-red-50 border border-red-200 rounded-xl flex items-start gap-3">
|
|
<div className="flex-shrink-0 w-5 h-5 bg-red-100 rounded-full flex items-center justify-center">
|
|
<span className="text-red-600 text-xs font-bold">!</span>
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-sm text-red-800 font-medium">Erreur</p>
|
|
<p className="text-sm text-red-600">{error}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Nom */}
|
|
<div className="space-y-2">
|
|
<label className="flex items-center gap-2 text-sm font-medium text-slate-700">
|
|
<FileText className="w-4 h-4 text-slate-500" />
|
|
Nom de la production *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Ex: Festival d'Avignon 2025"
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Référence */}
|
|
<div className="space-y-2">
|
|
<label className="flex items-center gap-2 text-sm font-medium text-slate-700">
|
|
<Hash className="w-4 h-4 text-slate-500" />
|
|
Numéro d'objet / Référence
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={reference}
|
|
onChange={(e) => setReference(e.target.value)}
|
|
placeholder="Ex: PROD-2025-001"
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Date de déclaration */}
|
|
<div className="space-y-2">
|
|
<label className="flex items-center gap-2 text-sm font-medium text-slate-700">
|
|
<Calendar className="w-4 h-4 text-slate-500" />
|
|
Date de déclaration
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={declarationDate}
|
|
onChange={(e) => setDeclarationDate(e.target.value)}
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Organisation */}
|
|
<div className="space-y-2">
|
|
<label className="flex items-center gap-2 text-sm font-medium text-slate-700">
|
|
<Building2 className="w-4 h-4 text-slate-500" />
|
|
Organisation *
|
|
</label>
|
|
<select
|
|
value={orgId}
|
|
onChange={(e) => setOrgId(e.target.value)}
|
|
className="w-full px-4 py-3 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
|
required
|
|
>
|
|
<option value="">Sélectionnez une organisation</option>
|
|
{organizations.map((org) => (
|
|
<option key={org.id} value={org.id}>
|
|
{org.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-3 pt-4 border-t">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
|
disabled={mutation.isPending}
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={mutation.isPending}
|
|
className="px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 rounded-lg transition-all shadow-sm hover:shadow-md disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{mutation.isPending && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
{production ? "Enregistrer" : "Créer"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Composant Modal de suppression
|
|
function DeleteConfirmModal({
|
|
isOpen,
|
|
onClose,
|
|
production,
|
|
}: {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
production: Production | null;
|
|
}) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (!production) return;
|
|
const res = await fetch(`/api/staff/productions/${production.id}`, {
|
|
method: "DELETE",
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
throw new Error(data.error || "Erreur lors de la suppression");
|
|
}
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["staff-productions"] });
|
|
onClose();
|
|
},
|
|
});
|
|
|
|
if (!isOpen || !production) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div
|
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
/>
|
|
<div className="relative w-full max-w-md bg-white rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className="flex-shrink-0 w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
|
|
<Trash2 className="w-6 h-6 text-red-600" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h3 className="text-lg font-semibold text-slate-900">
|
|
Confirmer la suppression
|
|
</h3>
|
|
<p className="text-sm text-slate-600 mt-1">
|
|
Cette action est irréversible
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 bg-slate-50 rounded-xl mb-6">
|
|
<p className="text-sm text-slate-700">
|
|
Voulez-vous vraiment supprimer la production{" "}
|
|
<span className="font-semibold">{production.name}</span> ?
|
|
</p>
|
|
{production.reference && (
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
Référence : {production.reference}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-lg transition-colors"
|
|
disabled={deleteMutation.isPending}
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
onClick={() => deleteMutation.mutate()}
|
|
disabled={deleteMutation.isPending}
|
|
className="px-4 py-2.5 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{deleteMutation.isPending && (
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
)}
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Page principale
|
|
export default function GestionProductionsPage() {
|
|
usePageTitle("Gestion des productions");
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [selectedOrgId, setSelectedOrgId] = useState("");
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
const [editingProduction, setEditingProduction] = useState<Production | null>(null);
|
|
const [deletingProduction, setDeletingProduction] = useState<Production | null>(null);
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
|
|
const { data: staffCheck, isLoading: isLoadingStaff } = useStaffCheck();
|
|
const { data: organizations = [], isLoading: isLoadingOrgs } = useOrganizations();
|
|
const { data: productions = [], isLoading: isLoadingProductions, error } = useProductions(
|
|
selectedOrgId,
|
|
searchQuery
|
|
);
|
|
|
|
// Filtrage local supplémentaire si nécessaire
|
|
const filteredProductions = useMemo(() => {
|
|
let result = productions;
|
|
|
|
if (searchQuery.trim()) {
|
|
const query = searchQuery.toLowerCase();
|
|
result = result.filter(
|
|
(p) =>
|
|
p.name.toLowerCase().includes(query) ||
|
|
(p.reference && p.reference.toLowerCase().includes(query))
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}, [productions, searchQuery]);
|
|
|
|
// Vérification staff
|
|
if (isLoadingStaff) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<Loader2 className="w-8 h-8 animate-spin text-indigo-600" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!staffCheck?.isStaff) {
|
|
return (
|
|
<main className="p-6">
|
|
<h1 className="text-lg font-semibold">Accès refusé</h1>
|
|
<p className="text-sm text-slate-600">Cette page est réservée au Staff.</p>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const hasActiveFilters = selectedOrgId || searchQuery;
|
|
|
|
return (
|
|
<div className="space-y-5 p-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">Gestion des productions</h1>
|
|
<p className="text-sm text-slate-600 mt-1">
|
|
Gérez les productions et spectacles de toutes les organisations
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsCreateModalOpen(true)}
|
|
className="inline-flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-xl hover:from-indigo-700 hover:to-purple-700 transition-all shadow-sm hover:shadow-md"
|
|
>
|
|
<Plus className="w-5 h-5" />
|
|
Nouvelle production
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filtres et recherche */}
|
|
<section className="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
|
<div className="p-4 border-b bg-slate-50/50">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Rechercher par nom ou référence..."
|
|
className="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
onClick={() => setSearchQuery("")}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 hover:bg-slate-100 rounded-md transition-colors"
|
|
>
|
|
<X className="w-4 h-4 text-slate-400" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => setShowFilters(!showFilters)}
|
|
className={`inline-flex items-center gap-2 px-4 py-2.5 rounded-xl border transition-all ${
|
|
hasActiveFilters
|
|
? "bg-indigo-50 border-indigo-200 text-indigo-700"
|
|
: "bg-white border-slate-200 text-slate-700 hover:bg-slate-50"
|
|
}`}
|
|
>
|
|
<Filter className="w-4 h-4" />
|
|
Filtres
|
|
{hasActiveFilters && (
|
|
<span className="px-2 py-0.5 bg-indigo-600 text-white text-xs rounded-full">
|
|
{selectedOrgId ? 1 : 0}
|
|
</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{hasActiveFilters && (
|
|
<button
|
|
onClick={() => {
|
|
setSelectedOrgId("");
|
|
setSearchQuery("");
|
|
}}
|
|
className="inline-flex items-center gap-1 text-sm text-slate-600 hover:text-slate-900 transition-colors"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
Réinitialiser
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{showFilters && (
|
|
<div className="mt-4 pt-4 border-t">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-slate-700">
|
|
Organisation
|
|
</label>
|
|
<select
|
|
value={selectedOrgId}
|
|
onChange={(e) => setSelectedOrgId(e.target.value)}
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
>
|
|
<option value="">Toutes les organisations</option>
|
|
{organizations.map((org) => (
|
|
<option key={org.id} value={org.id}>
|
|
{org.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="px-4 py-3 bg-gradient-to-r from-slate-50 to-indigo-50/30 border-b">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-slate-600">
|
|
{isLoadingProductions ? (
|
|
<span className="flex items-center gap-2">
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Chargement...
|
|
</span>
|
|
) : (
|
|
<>
|
|
<span className="font-semibold text-slate-900">
|
|
{filteredProductions.length}
|
|
</span>{" "}
|
|
production{filteredProductions.length > 1 ? "s" : ""}
|
|
{hasActiveFilters && " (filtrée" + (filteredProductions.length > 1 ? "s" : "") + ")"}
|
|
</>
|
|
)}
|
|
</span>
|
|
{selectedOrgId && (
|
|
<span className="text-slate-600">
|
|
Organisation :{" "}
|
|
<span className="font-medium text-slate-900">
|
|
{organizations.find((o) => o.id === selectedOrgId)?.name}
|
|
</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tableau */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-slate-50 border-b">
|
|
<tr>
|
|
<th className="text-left px-6 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">
|
|
Nom
|
|
</th>
|
|
<th className="text-left px-6 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">
|
|
Référence
|
|
</th>
|
|
<th className="text-left px-6 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">
|
|
Date déclaration
|
|
</th>
|
|
<th className="text-left px-6 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">
|
|
Organisation
|
|
</th>
|
|
<th className="text-right px-6 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{isLoadingProductions ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-6 py-12 text-center">
|
|
<Loader2 className="w-6 h-6 animate-spin mx-auto text-indigo-600" />
|
|
<p className="text-sm text-slate-600 mt-2">
|
|
Chargement des productions...
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
) : error ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-6 py-12 text-center">
|
|
<p className="text-sm text-red-600">
|
|
Erreur lors du chargement des productions
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
) : filteredProductions.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-6 py-12 text-center">
|
|
<Building2 className="w-12 h-12 mx-auto text-slate-300 mb-3" />
|
|
<p className="text-sm font-medium text-slate-600">
|
|
Aucune production trouvée
|
|
</p>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
{hasActiveFilters
|
|
? "Essayez de modifier vos filtres"
|
|
: "Créez votre première production"}
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
filteredProductions.map((production) => {
|
|
const org = organizations.find((o) => o.id === production.org_id);
|
|
return (
|
|
<tr
|
|
key={production.id}
|
|
className="hover:bg-slate-50/50 transition-colors"
|
|
>
|
|
<td className="px-6 py-4">
|
|
<div className="font-medium text-slate-900">
|
|
{production.name}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
{production.reference ? (
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-slate-100 text-slate-700 rounded-lg text-xs font-medium">
|
|
<Hash className="w-3 h-3" />
|
|
{production.reference}
|
|
</span>
|
|
) : (
|
|
<span className="text-slate-400 text-sm">—</span>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
{production.declaration_date ? (
|
|
<div className="flex items-center gap-2 text-sm text-slate-600">
|
|
<Calendar className="w-4 h-4 text-slate-400" />
|
|
{new Date(production.declaration_date).toLocaleDateString("fr-FR")}
|
|
</div>
|
|
) : (
|
|
<span className="text-slate-400 text-sm">—</span>
|
|
)}
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<span className="text-sm text-slate-600">
|
|
{org?.name || "—"}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={() => setEditingProduction(production)}
|
|
className="p-2 hover:bg-indigo-50 rounded-lg transition-colors group"
|
|
title="Modifier"
|
|
>
|
|
<Edit2 className="w-4 h-4 text-slate-400 group-hover:text-indigo-600" />
|
|
</button>
|
|
<button
|
|
onClick={() => setDeletingProduction(production)}
|
|
className="p-2 hover:bg-red-50 rounded-lg transition-colors group"
|
|
title="Supprimer"
|
|
>
|
|
<Trash2 className="w-4 h-4 text-slate-400 group-hover:text-red-600" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Modals */}
|
|
<ProductionModal
|
|
isOpen={isCreateModalOpen || !!editingProduction}
|
|
onClose={() => {
|
|
setIsCreateModalOpen(false);
|
|
setEditingProduction(null);
|
|
}}
|
|
production={editingProduction}
|
|
organizations={organizations}
|
|
/>
|
|
|
|
<DeleteConfirmModal
|
|
isOpen={!!deletingProduction}
|
|
onClose={() => setDeletingProduction(null)}
|
|
production={deletingProduction}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|