diff --git a/TEMPS_TRAVAIL_REEL.md b/TEMPS_TRAVAIL_REEL.md new file mode 100644 index 0000000..bf935b1 --- /dev/null +++ b/TEMPS_TRAVAIL_REEL.md @@ -0,0 +1,108 @@ +# Temps de Travail Réel - Fonctionnalité + +## 📋 Contexte + +Depuis la migration vers le nouvel Espace Paie, le système de saisie des heures et jours de travail a changé. Les données affichées dans la card "Temps de travail réel" sur la page client `contrats/[id]` n'étaient plus correctement alimentées. + +## ✅ Solution Implémentée + +### 1. Nouvelles Colonnes Supabase + +Ajout de colonnes dédiées dans `cddu_contracts` pour stocker le temps de travail **réel** (distinct des données contractuelles) : + +| Colonne | Type | Description | +|---------|------|-------------| +| `jours_travail_reel` | TEXT | Jours de travail réels (artiste) | +| `jours_travail_non_artiste_reel` | TEXT | Jours de travail réels (technicien) | +| `nb_representations_reel` | INTEGER | Nombre réel de représentations | +| `dates_representations_reel` | TEXT | Dates réelles des représentations | +| `nb_services_repetitions_reel` | INTEGER | Nombre réel de services de répétition | +| `nb_heures_repetitions_reel` | NUMERIC(10,2) | Heures réelles de répétition | +| `dates_repetitions_reel` | TEXT | Dates réelles des répétitions | +| `nb_heures_annexes_reel` | NUMERIC(10,2) | Heures réelles Annexes 8 | +| `nb_cachets_aem_reel` | INTEGER | Cachets AEM réels | +| `nb_heures_aem_reel` | NUMERIC(10,2) | Heures AEM réelles | + +**Note importante** : Ces colonnes sont purement **informatives pour le client** et ne sont **pas utilisées** pour la génération du PDF du contrat via PDFMonkey. + +### 2. Interface Staff (staff/contrats/[id]) + +Ajout d'une **sous-section "Temps de travail réel"** dans la card "Temps de travail" : + +- **Emplacement** : Après le séparateur, en bas de la card "Temps de travail" +- **Indication visuelle** : Message clair indiquant que c'est informatif pour le client +- **Champs disponibles** : + - Jours travaillés (artiste) + - Jours travaillés (technicien) + - Nombre de représentations + - Dates de représentations + - Nombre de services répétitions + - Nombre d'heures répétitions + - Dates de répétitions + - Nombre d'heures Annexes 8 + - Nombre de cachets AEM + - Nombre d'heures AEM + +### 3. Affichage Client (contrats/[id]) + +La card "Temps de travail réel" affiche maintenant les données des colonnes `*_reel` : + +```tsx +
+ + + // etc. +
+``` + +### 4. API Mapping + +L'API `/api/contrats/[id]` mappe automatiquement ces nouvelles colonnes : + +```typescript +nb_representations_reel: cddu.nb_representations_reel ? Number(cddu.nb_representations_reel) : undefined, +nb_services_repetitions_reel: cddu.nb_services_repetitions_reel ? Number(cddu.nb_services_repetitions_reel) : undefined, +// etc. +``` + +## 🚀 Migration SQL + +Exécuter le script de migration : + +```bash +# Via Supabase Dashboard > SQL Editor +# Ou via CLI supabase +supabase db execute -f migrations/add_temps_travail_reel_columns.sql +``` + +## 📝 Utilisation + +1. **Staff** : Aller sur `staff/contrats/[id]` +2. Descendre jusqu'à la card **"Temps de travail"** +3. Remplir la sous-section **"Temps de travail réel (informatif client)"** +4. Cliquer sur **Sauvegarder** +5. **Client** : Les données apparaissent immédiatement dans `contrats/[id]` > "Temps de travail réel" + +## ⚠️ Points d'Attention + +- ✅ Ces données sont **indépendantes** des données contractuelles (utilisées pour le PDF) +- ✅ Elles **ne sont pas envoyées** à PDFMonkey +- ✅ Elles sont **purement informatives** pour le client +- ✅ Les anciennes colonnes (`jours_travail`, `nb_representations`, etc.) restent utilisées pour le contrat PDF + +## 📂 Fichiers Modifiés + +| Fichier | Modifications | +|---------|---------------| +| `migrations/add_temps_travail_reel_columns.sql` | Création des colonnes | +| `components/staff/contracts/ContractEditor.tsx` | Ajout UI + états + sauvegarde | +| `app/api/contrats/[id]/route.ts` | Mapping API | +| `app/(app)/contrats/[id]/page.tsx` | Affichage client | + +## ✅ Tests + +- [ ] Exécuter la migration SQL sur Supabase +- [ ] Vérifier que les colonnes sont créées +- [ ] Tester la saisie dans staff/contrats/[id] +- [ ] Vérifier l'affichage dans contrats/[id] +- [ ] Vérifier que le PDF du contrat n'est pas impacté diff --git a/app/(app)/contrats/[id]/page.tsx b/app/(app)/contrats/[id]/page.tsx index 1c0a154..2e01b5e 100644 --- a/app/(app)/contrats/[id]/page.tsx +++ b/app/(app)/contrats/[id]/page.tsx @@ -115,7 +115,7 @@ function usePayslips(contractId: string) { import { useParams, useRouter } from "next/navigation"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { api } from "@/lib/fetcher"; -import { Loader2, ArrowLeft, Check, Pencil, Download, Info, AlertTriangle, CheckCircle, Clock, Copy, PenTool, XCircle, Users, Send, FileText, CreditCard, Shield, Calendar, StickyNote, Euro } from "lucide-react"; +import { Loader2, ArrowLeft, Check, Pencil, Download, Info, AlertTriangle, CheckCircle, Clock, Copy, PenTool, XCircle, Users, Send, FileText, CreditCard, Shield, Calendar, StickyNote, Euro, HelpCircle, Repeat, Timer, Wrench, Theater, TrendingUp } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { NotesSection } from "@/components/NotesSection"; @@ -1563,20 +1563,139 @@ return (
- - - - - - - +
+
+ {/* Jours travaillés */} +
+
+
+ +
+
+
+ Jours travaillés +
+
+ {data.jours_travail_reel || "—"} +
+
+
+
+ + {/* Représentations */} +
+
+
+ Représentations +
+
+
+ {data.nb_representations_reel ?? 0} +
+
cachets
+
+ + {/* Services répétitions */} +
+
+
+ Services répétitions +
+
+ +
+ Indique le nombre de services de répétitions réels, indépendamment du type de rémunération (au cachet ou à l'heure). +
+
+
+
+
+ {data.nb_services_repetitions_reel ?? 0} +
+
services
+
+ + {/* Heures répétitions */} +
+
+
+ Heures répétitions +
+
+ +
+ Indique le nombre d'heures de répétitions réel, indépendamment du type de rémunération (au cachet ou à l'heure). +
+
+
+
+
+ {data.nb_heures_repetitions_reel ?? 0} +
+
heures
+
+ + {/* Heures Annexe 8 */} +
+
+
+ Heures Annexe 8 +
+
+ +
+ Concerne uniquement les techniciens (Annexe 8). Les techniciens ne peuvent être rémunérés qu'à l'heure. +
+
+
+
+
+ {data.nb_heures_annexes_reel ?? 0} +
+
heures
+
+ + {/* Cachets AEM */} +
+
+
+ Cachets AEM +
+
+ +
+ Indique le nombre de cachets de représentation et/ou de répétitions (uniquement pour les répétitions pouvant être rémunérées au cachet) ayant été déclaré dans l'AEM. +
+
+
+
+
+ {data.nb_cachets_aem_reel ?? 0} +
+
cachets
+
+ + {/* Heures AEM */} +
+
+
+ Heures AEM +
+
+ +
+ Indique les heures déclarées dans l'AEM (contrat technicien, metteur en scène ou répétitions ne pouvant être rémunérées en cachet). +
+
+
+
+
+ {data.nb_heures_aem_reel ?? 0} +
+
heures
+
+
+
diff --git a/app/(staff)/staff/contrats/saisie-temps-reel/page.tsx b/app/(staff)/staff/contrats/saisie-temps-reel/page.tsx new file mode 100644 index 0000000..b0adb7e --- /dev/null +++ b/app/(staff)/staff/contrats/saisie-temps-reel/page.tsx @@ -0,0 +1,619 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { supabase } from "@/lib/supabaseClient"; +import { api } from "@/lib/fetcher"; +import { ArrowLeft, Search, Filter, ChevronDown, Save, CheckCircle2 } from "lucide-react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { toast } from "sonner"; + +type Contract = { + id: string; + production_name: string; + employee_name: string; + profession: string; + start_date: string; + end_date: string; + org_id: string; + organization_name?: string; + // Temps de travail contractuel (référence) + jours_travail: string | null; + jours_travail_non_artiste: string | null; + jours_representations: string | null; + jours_repetitions: string | null; + cachets_representations: number | null; + services_repetitions: number | null; + nombre_d_heures: number | null; + precisions_salaire: string | null; + // Temps de travail réel (éditable) + jours_travail_reel: string | null; + nb_representations_reel: number | null; + nb_services_repetitions_reel: number | null; + nb_heures_repetitions_reel: number | null; + nb_heures_annexes_reel: number | null; + nb_cachets_aem_reel: number | null; + nb_heures_aem_reel: number | null; + temps_reel_traite: boolean | null; +}; + +export default function SaisieTempsReelPage() { + const queryClient = useQueryClient(); + + // Filtres + const [yearFilter, setYearFilter] = useState("all"); + const [monthFilter, setMonthFilter] = useState("all"); + const [orgFilter, setOrgFilter] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState<"production" | "employee" | "date">("date"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + const [showTraites, setShowTraites] = useState(false); + + // États d'édition locale + const [editedCells, setEditedCells] = useState>({}); + const [savingCells, setSavingCells] = useState>(new Set()); + + // Récupération des contrats + const { data: contracts = [], isLoading, error } = useQuery({ + queryKey: ["contracts-temps-reel", yearFilter, monthFilter, orgFilter], + queryFn: async () => { + const params = new URLSearchParams(); + if (yearFilter !== "all") params.set("year", yearFilter); + if (monthFilter !== "all") params.set("month", monthFilter); + if (orgFilter !== "all") params.set("org", orgFilter); + + const url = `/staff/contrats/temps-reel?${params.toString()}`; + const data = await api(url) as Contract[]; + + console.log("✅ Contrats récupérés:", data?.length || 0); + return data || []; + }, + staleTime: 30_000, + }); + + console.log("📊 État query:", { isLoading, error, contractsCount: contracts.length }); + + // Récupération des organisations pour le filtre + const { data: organizations = [] } = useQuery>({ + queryKey: ["organizations-list"], + queryFn: async () => { + const response = await api("/staff/organizations") as { organizations: Array<{id: string; name: string}> }; + return response.organizations || []; + }, + }); + + // Mutation pour sauvegarder une cellule + const saveMutation = useMutation({ + mutationFn: async ({ contractId, field, value }: { contractId: string; field: string; value: any }) => { + console.log("[saveMutation] Début sauvegarde:", { contractId, field, value }); + + const response = await fetch("/api/staff/contrats/temps-reel", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ contractId, field, value }), + }); + + if (!response.ok) { + const error = await response.json(); + console.error("[saveMutation] Erreur réponse:", error); + throw new Error(error.error || "Erreur de sauvegarde"); + } + + const data = await response.json(); + console.log("[saveMutation] Succès:", data); + return { contractId, field, value }; + }, + onMutate: ({ contractId, field }) => { + console.log("[saveMutation] onMutate:", { contractId, field }); + setSavingCells((prev) => new Set(prev).add(`${contractId}-${field}`)); + }, + onSuccess: ({ contractId, field, value }) => { + console.log("[saveMutation] onSuccess:", { contractId, field, value }); + toast.success("Sauvegardé"); + setSavingCells((prev) => { + const next = new Set(prev); + next.delete(`${contractId}-${field}`); + return next; + }); + queryClient.invalidateQueries({ queryKey: ["contracts-temps-reel"] }); + }, + onError: (error, { contractId, field }) => { + console.error("[saveMutation] onError:", error, { contractId, field }); + toast.error(`Erreur: ${error.message}`); + setSavingCells((prev) => { + const next = new Set(prev); + next.delete(`${contractId}-${field}`); + return next; + }); + }, + }); + + // Fonction pour gérer le changement d'une cellule + const handleCellChange = (contractId: string, field: string, value: any) => { + const key = `${contractId}-${field}`; + console.log("[handleCellChange]", { contractId, field, value, key }); + setEditedCells((prev) => ({ ...prev, [key]: value })); + }; + + // Fonction pour sauvegarder une cellule (au blur ou Enter) + const handleCellSave = (contractId: string, field: string) => { + const key = `${contractId}-${field}`; + const value = editedCells[key]; + + console.log("[handleCellSave]", { contractId, field, key, value, editedCells }); + + if (value !== undefined) { + saveMutation.mutate({ contractId, field, value }); + setEditedCells((prev) => { + const next = { ...prev }; + delete next[key]; + return next; + }); + } else { + console.log("[handleCellSave] Pas de valeur à sauvegarder"); + } + }; + + // Filtrer et trier les contrats + const filteredContracts = contracts + .filter((contract) => { + // Filtre par recherche + if (searchQuery) { + const query = searchQuery.toLowerCase(); + const matches = ( + contract.production_name?.toLowerCase().includes(query) || + contract.employee_name?.toLowerCase().includes(query) || + contract.profession?.toLowerCase().includes(query) + ); + if (!matches) return false; + } + // Filtre par statut traité (masquer les traités par défaut) + if (!showTraites && contract.temps_reel_traite) return false; + return true; + }) + .sort((a, b) => { + let aVal, bVal; + if (sortBy === "production") { + aVal = a.production_name || ""; + bVal = b.production_name || ""; + } else if (sortBy === "employee") { + aVal = a.employee_name || ""; + bVal = b.employee_name || ""; + } else { + aVal = a.start_date || ""; + bVal = b.start_date || ""; + } + + if (sortOrder === "asc") { + return aVal > bVal ? 1 : -1; + } else { + return aVal < bVal ? 1 : -1; + } + }); + + // Générer les années disponibles (5 dernières années) + const currentYear = new Date().getFullYear(); + const years = Array.from({ length: 5 }, (_, i) => currentYear - i); + + return ( +
+
+ {/* Header */} +
+ + + +

Saisie Temps de Travail Réel

+

+ Saisissez rapidement les données réelles pour alimenter la vue client. Cliquez sur une cellule pour modifier, les modifications sont sauvegardées automatiquement. +

+
+ + {/* Filtres */} + + +
+ {/* Recherche */} +
+ +
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+ + {/* Année */} +
+ + +
+ + {/* Mois */} +
+ + +
+ + {/* Organisation */} +
+ + +
+
+ + {/* Tri et affichage des traités */} +
+
+ Trier par : +
+ + +
+
+ + {/* Toggle pour afficher les traités */} + +
+
+
+ + {/* Tableau */} + + +
+ + + + + + + + + + + + + + + + + + + + + + + {isLoading ? ( + + + + ) : error ? ( + + + + ) : filteredContracts.length === 0 ? ( + + + + ) : ( + filteredContracts.map((contract) => ( + + {/* Salarié */} + + {/* Profession */} + + {/* Dates */} + + {/* TT Contractuel (référence) */} + + {/* Cachets représentations (PDFMonkey) */} + + {/* Services répétitions (PDFMonkey) */} + + {/* Nombre d'heures (PDFMonkey) */} + + {/* Précisions salaire (référence) */} + + {/* Jours travaillés réel */} + + {/* Nb représentations réel */} + + {/* Nb services répétitions réel */} + + {/* Nb heures répétitions réel */} + + {/* Nb heures Annexe 8 réel */} + + {/* Nb cachets AEM réel */} + + {/* Nb heures AEM réel */} + + {/* Checkbox traité */} + + + )) + )} + +
SalariéProfessionDatesTT ContractuelCach.Serv.HeuresPrécisionsJours réelsRepr.Serv. rép.H. rép.H. Ann.8Cach. AEMH. AEMTraité
+ Chargement des contrats... +
+
Erreur de chargement
+
{String(error)}
+
+
Aucun contrat trouvé avec ces filtres.
+
Total dans la base : {contracts.length}
+
+
+ {contract.employee_name || "—"} +
+
+ {contract.profession || "—"} + + {contract.start_date ? new Date(contract.start_date).toLocaleDateString("fr-FR") : "—"} + +
+ {(() => { + const parts = []; + if (contract.jours_travail_non_artiste) parts.push(contract.jours_travail_non_artiste); + if (contract.jours_representations) parts.push(contract.jours_representations); + if (contract.jours_repetitions) parts.push(contract.jours_repetitions); + if (contract.jours_travail && !contract.jours_representations && !contract.jours_repetitions) parts.push(contract.jours_travail); + return parts.length > 0 ? parts.join(" + ") : "—"; + })()} +
+
+ {contract.cachets_representations ?? "—"} + + {contract.services_repetitions ?? "—"} + + {contract.nombre_d_heures ?? "—"} + +
+ {contract.precisions_salaire || "—"} +
+
+ + + + + + + + + + + + + + + { + saveMutation.mutate({ + contractId: contract.id, + field: "temps_reel_traite", + value: e.target.checked, + }); + }} + className="w-4 h-4 rounded border-slate-300 cursor-pointer" + title="Marquer comme traité" + /> +
+
+
+
+ + {/* Statistiques */} + {filteredContracts.length > 0 && ( +
+ {filteredContracts.length} contrat{filteredContracts.length > 1 ? "s" : ""} affiché{filteredContracts.length > 1 ? "s" : ""} +
+ )} +
+
+ ); +} + +// Composant pour cellule éditable +function EditableCell({ + contractId, + field, + value, + editedValue, + onChange, + onSave, + isSaving, + type, +}: { + contractId: string; + field: string; + value: string | number | null; + editedValue: any; + onChange: (contractId: string, field: string, value: any) => void; + onSave: (contractId: string, field: string) => void; + isSaving: boolean; + type: "text" | "number"; +}) { + const displayValue = editedValue !== undefined ? editedValue : (value ?? ""); + const isEmpty = !value && editedValue === undefined; + + return ( +
+ { + const val = type === "number" ? (e.target.value === "" ? null : Number(e.target.value)) : e.target.value; + onChange(contractId, field, val); + }} + onBlur={() => onSave(contractId, field)} + onKeyDown={(e) => { + if (e.key === "Enter") { + onSave(contractId, field); + e.currentTarget.blur(); + } + }} + className={`w-full px-2 py-1 text-sm border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 ${ + isEmpty ? "border-red-200 bg-red-50" : "border-slate-200" + } ${isSaving ? "opacity-50" : ""}`} + placeholder={type === "number" ? "0" : "—"} + disabled={isSaving} + /> + {isSaving && ( +
+
+
+ )} +
+ ); +} diff --git a/app/api/contrats/[id]/route.ts b/app/api/contrats/[id]/route.ts index 16ec53c..c759670 100644 --- a/app/api/contrats/[id]/route.ts +++ b/app/api/contrats/[id]/route.ts @@ -182,6 +182,17 @@ export async function GET(req: NextRequest, { params }: { params: { id: string } nb_heures_annexes: cddu.heures_annexe_8 ? Number(cddu.heures_annexe_8) : undefined, nb_cachets_aem: cddu.cachets ? Number(cddu.cachets) : undefined, nb_heures_aem: cddu.nombre_d_heures ? Number(cddu.nombre_d_heures) : undefined, + // Temps de travail réel (nouveaux champs informatifs) + jours_travail_reel: cddu.jours_travail_reel || undefined, + jours_travail_non_artiste_reel: cddu.jours_travail_non_artiste_reel || undefined, + nb_representations_reel: cddu.nb_representations_reel ? Number(cddu.nb_representations_reel) : undefined, + dates_representations_reel: cddu.dates_representations_reel || undefined, + nb_services_repetitions_reel: cddu.nb_services_repetitions_reel ? Number(cddu.nb_services_repetitions_reel) : undefined, + nb_heures_repetitions_reel: cddu.nb_heures_repetitions_reel ? Number(cddu.nb_heures_repetitions_reel) : undefined, + dates_repetitions_reel: cddu.dates_repetitions_reel || undefined, + nb_heures_annexes_reel: cddu.nb_heures_annexes_reel ? Number(cddu.nb_heures_annexes_reel) : undefined, + nb_cachets_aem_reel: cddu.nb_cachets_aem_reel ? Number(cddu.nb_cachets_aem_reel) : undefined, + nb_heures_aem_reel: cddu.nb_heures_aem_reel ? Number(cddu.nb_heures_aem_reel) : undefined, created_at: cddu.created_at, updated_at: cddu.updated_at, }; @@ -353,6 +364,28 @@ export async function PATCH(req: NextRequest, { params }: { params: { id: string if (requestBody.jours_travail_non_artiste !== undefined) { supabaseData.jours_travail_non_artiste = requestBody.jours_travail_non_artiste; } + // Temps de travail réel (informatif pour le client) + if (requestBody.jours_travail_reel !== undefined) { + supabaseData.jours_travail_reel = requestBody.jours_travail_reel; + } + if (requestBody.nb_representations_reel !== undefined) { + supabaseData.nb_representations_reel = requestBody.nb_representations_reel; + } + if (requestBody.nb_services_repetitions_reel !== undefined) { + supabaseData.nb_services_repetitions_reel = requestBody.nb_services_repetitions_reel; + } + if (requestBody.nb_heures_repetitions_reel !== undefined) { + supabaseData.nb_heures_repetitions_reel = requestBody.nb_heures_repetitions_reel; + } + if (requestBody.nb_heures_annexes_reel !== undefined) { + supabaseData.nb_heures_annexes_reel = requestBody.nb_heures_annexes_reel; + } + if (requestBody.nb_cachets_aem_reel !== undefined) { + supabaseData.nb_cachets_aem_reel = requestBody.nb_cachets_aem_reel; + } + if (requestBody.nb_heures_aem_reel !== undefined) { + supabaseData.nb_heures_aem_reel = requestBody.nb_heures_aem_reel; + } if (requestBody.type_salaire !== undefined) { supabaseData.type_salaire = requestBody.type_salaire; } @@ -549,7 +582,13 @@ export async function PATCH(req: NextRequest, { params }: { params: { id: string 'cachets_representations', 'services_repetitions', 'jours_representations', 'jours_repetitions', 'nombre_d_heures', 'jours_travail', 'jours_travail_non_artiste', 'notes', - 'dates_travaillees', 'type_salaire', 'montant', 'panier_repas' + 'dates_travaillees', 'type_salaire', 'montant', 'panier_repas', + // Temps de travail réel + 'jours_travail_reel', + 'nb_representations_reel', + 'nb_services_repetitions_reel', 'nb_heures_repetitions_reel', + 'nb_heures_annexes_reel', + 'nb_cachets_aem_reel', 'nb_heures_aem_reel' ]; fieldsToSync.forEach(field => { diff --git a/app/api/staff/contracts/[id]/route.ts b/app/api/staff/contracts/[id]/route.ts index 0969afe..193416a 100644 --- a/app/api/staff/contracts/[id]/route.ts +++ b/app/api/staff/contracts/[id]/route.ts @@ -65,7 +65,13 @@ const CONTRACT_UPDATABLE_FIELDS = new Set([ "si_non_montant_par_indemnite", "mineur_entre_16_et_18", "civilite_representant_legal", "nom_representant_legal", "adresse_representant_legal", "dob_representant_legal", "cob_representant_legal", "periode", "docuseal_template_id", "docuseal_submission_id", - "motif_cdd", "contract_kind", "date_signature" + "motif_cdd", "contract_kind", "date_signature", + // Temps de travail réel (informatif pour le client) + "jours_travail_reel", + "nb_representations_reel", + "nb_services_repetitions_reel", "nb_heures_repetitions_reel", + "nb_heures_annexes_reel", + "nb_cachets_aem_reel", "nb_heures_aem_reel" ]); export async function GET(_req: Request, { params }: { params: { id: string } }) { diff --git a/app/api/staff/contrats/temps-reel/route.ts b/app/api/staff/contrats/temps-reel/route.ts new file mode 100644 index 0000000..f1aced5 --- /dev/null +++ b/app/api/staff/contrats/temps-reel/route.ts @@ -0,0 +1,166 @@ +import { NextResponse } from "next/server"; +import { createSbServer } from "@/lib/supabaseServer"; + +export async function GET(req: Request) { + const supabase = createSbServer(); + + // Vérifier l'authentification + const { data: { user }, error: authError } = await supabase.auth.getUser(); + if (authError || !user) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + // Vérifier que l'utilisateur est staff + const { data: staffUser } = await supabase + .from("staff_users") + .select("is_staff") + .eq("user_id", user.id) + .maybeSingle(); + + if (!staffUser?.is_staff) { + return NextResponse.json({ error: "Accès réservé au staff" }, { status: 403 }); + } + + // Récupérer les paramètres de filtrage depuis l'URL + const { searchParams } = new URL(req.url); + const yearFilter = searchParams.get("year"); + const monthFilter = searchParams.get("month"); + const orgFilter = searchParams.get("org"); + + try { + let query = supabase + .from("cddu_contracts") + .select(` + id, + production_name, + employee_name, + profession, + start_date, + end_date, + org_id, + jours_travail, + jours_travail_non_artiste, + jours_representations, + jours_repetitions, + cachets_representations, + services_repetitions, + nombre_d_heures, + precisions_salaire, + jours_travail_reel, + nb_representations_reel, + nb_services_repetitions_reel, + nb_heures_repetitions_reel, + nb_heures_annexes_reel, + nb_cachets_aem_reel, + nb_heures_aem_reel, + temps_reel_traite, + organizations(name) + `); + + // Filtre par année + if (yearFilter && yearFilter !== "all") { + const yearStart = `${yearFilter}-01-01`; + const yearEnd = `${yearFilter}-12-31`; + query = query.gte("start_date", yearStart).lte("start_date", yearEnd); + } + + // Filtre par mois + if (monthFilter && monthFilter !== "all" && yearFilter && yearFilter !== "all") { + const monthPadded = monthFilter.padStart(2, "0"); + const monthStart = `${yearFilter}-${monthPadded}-01`; + const nextMonth = parseInt(monthFilter) === 12 ? 1 : parseInt(monthFilter) + 1; + const nextYear = parseInt(monthFilter) === 12 ? parseInt(yearFilter) + 1 : parseInt(yearFilter); + const monthEnd = `${nextYear}-${String(nextMonth).padStart(2, "0")}-01`; + query = query.gte("start_date", monthStart).lt("start_date", monthEnd); + } + + // Filtre par organisation + if (orgFilter && orgFilter !== "all") { + query = query.eq("org_id", orgFilter); + } + + const { data, error } = await query.order("start_date", { ascending: false }); + + if (error) { + console.error("Erreur récupération contrats:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + // Ajouter le nom de l'organisation + const contracts = (data || []).map((contract: any) => ({ + ...contract, + organization_name: contract.organizations?.name || "—", + })); + + return NextResponse.json(contracts); + } catch (error: any) { + console.error("Erreur serveur:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} + +export async function PATCH(req: Request) { + const supabase = createSbServer(); + + // Vérifier l'authentification + const { data: { user }, error: authError } = await supabase.auth.getUser(); + if (authError || !user) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + // Vérifier que l'utilisateur est staff + const { data: staffUser } = await supabase + .from("staff_users") + .select("is_staff") + .eq("user_id", user.id) + .maybeSingle(); + + if (!staffUser?.is_staff) { + return NextResponse.json({ error: "Accès réservé au staff" }, { status: 403 }); + } + + try { + const body = await req.json(); + const { contractId, field, value } = body; + + console.log("[PATCH temps-reel] Mise à jour:", { contractId, field, value }); + + if (!contractId || !field) { + return NextResponse.json({ error: "contractId et field requis" }, { status: 400 }); + } + + // Liste des champs autorisés pour la mise à jour + const allowedFields = [ + "jours_travail_reel", + "nb_representations_reel", + "nb_services_repetitions_reel", + "nb_heures_repetitions_reel", + "nb_heures_annexes_reel", + "nb_cachets_aem_reel", + "nb_heures_aem_reel", + "temps_reel_traite", + ]; + + if (!allowedFields.includes(field)) { + return NextResponse.json({ error: "Champ non autorisé" }, { status: 400 }); + } + + const { data, error } = await supabase + .from("cddu_contracts") + .update({ [field]: value }) + .eq("id", contractId) + .select() + .single(); + + if (error) { + console.error("[PATCH temps-reel] Erreur:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + console.log("[PATCH temps-reel] Succès:", data); + return NextResponse.json({ success: true, data }); + } catch (error: any) { + console.error("[PATCH temps-reel] Erreur serveur:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index a2dfec0..ec0db21 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useState, useEffect, useRef } from "react"; -import { Shield, KeyRound, Home, FileSignature, Banknote, Users, Percent, FolderOpen, Receipt, Building2, UserCog, Edit3, LifeBuoy, CreditCard, Calculator, Euro, Mail, Database, Clapperboard, LogOut, Scale, FileEdit, FileText, Megaphone, Gift } from "lucide-react"; +import { Shield, KeyRound, Home, FileSignature, Banknote, Users, Percent, FolderOpen, Receipt, Building2, UserCog, Edit3, LifeBuoy, CreditCard, Calculator, Euro, Mail, Database, Clapperboard, LogOut, Scale, FileEdit, FileText, Megaphone, Gift, Clock } from "lucide-react"; // import { api } from "@/lib/fetcher"; import { createPortal } from "react-dom"; import LogoutButton from "@/components/LogoutButton"; @@ -558,6 +558,14 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o Contrats + onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${ + isActivePath(pathname, "/staff/contrats/saisie-temps-reel") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50" + }`} title="Saisie temps de travail réel"> + + + Temps de travail réel + + onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${ isActivePath(pathname, "/staff/payslips") ? "bg-gradient-to-r from-indigo-200 via-purple-200 to-pink-200 text-slate-700 font-semibold" : "hover:bg-slate-50" }`} title="Gestion des fiches de paie"> diff --git a/components/staff/contracts/ContractEditor.tsx b/components/staff/contracts/ContractEditor.tsx index 3f9aafd..d7d5b5c 100644 --- a/components/staff/contracts/ContractEditor.tsx +++ b/components/staff/contracts/ContractEditor.tsx @@ -327,6 +327,18 @@ export default function ContractEditor({ const [nombreHeuresTotal, setNombreHeuresTotal] = useState(contract.nombre_d_heures || ""); const [nombreHeuresParJour, setNombreHeuresParJour] = useState(contract.nombre_d_heures_par_jour || ""); + // États pour le temps de travail réel (informatif pour le client) + const [joursTravailReel, setJoursTravailReel] = useState(contract.jours_travail_reel || ""); + const [joursTravailNonArtisteReel, setJoursTravailNonArtisteReel] = useState(contract.jours_travail_non_artiste_reel || ""); + const [nbRepresentationsReel, setNbRepresentationsReel] = useState(contract.nb_representations_reel || ""); + const [datesRepresentationsReel, setDatesRepresentationsReel] = useState(contract.dates_representations_reel || ""); + const [nbServicesRepetitionsReel, setNbServicesRepetitionsReel] = useState(contract.nb_services_repetitions_reel || ""); + const [nbHeuresRepetitionsReel, setNbHeuresRepetitionsReel] = useState(contract.nb_heures_repetitions_reel || ""); + const [datesRepetitionsReel, setDatesRepetitionsReel] = useState(contract.dates_repetitions_reel || ""); + const [nbHeuresAnnexesReel, setNbHeuresAnnexesReel] = useState(contract.nb_heures_annexes_reel || ""); + const [nbCachetsAemReel, setNbCachetsAemReel] = useState(contract.nb_cachets_aem_reel || ""); + const [nbHeuresAemReel, setNbHeuresAemReel] = useState(contract.nb_heures_aem_reel || ""); + // Synchroniser les états avec les données du contrat useEffect(() => { // Calculer le mode (heures vs représentations/répétitions) @@ -508,6 +520,18 @@ export default function ContractEditor({ setPrecisionsSalaire(contract.precisions_salaire || ""); setAutrePrecisionDuree(contract.autreprecision_duree || ""); setAutrePrecisionSalaire(contract.autreprecision_salaire || ""); + + // Synchroniser les champs de temps de travail réel + setJoursTravailReel(contract.jours_travail_reel || ""); + setJoursTravailNonArtisteReel(contract.jours_travail_non_artiste_reel || ""); + setNbRepresentationsReel(contract.nb_representations_reel || ""); + setDatesRepresentationsReel(contract.dates_representations_reel || ""); + setNbServicesRepetitionsReel(contract.nb_services_repetitions_reel || ""); + setNbHeuresRepetitionsReel(contract.nb_heures_repetitions_reel || ""); + setDatesRepetitionsReel(contract.dates_repetitions_reel || ""); + setNbHeuresAnnexesReel(contract.nb_heures_annexes_reel || ""); + setNbCachetsAemReel(contract.nb_cachets_aem_reel || ""); + setNbHeuresAemReel(contract.nb_heures_aem_reel || ""); }, [contract, categoriePro, professionPick?.code]); // États pour les autres champs CDDU actuellement grisés @@ -1103,6 +1127,14 @@ export default function ContractEditor({ brut: typeSalaire === "Brut" ? parseMonetaryAmount(montant) : null, // Le champ gross_pay correspond au champ "Brut" de l'interface utilisateur gross_pay: parseMonetaryAmount(form.gross_pay), + // Temps de travail réel (informatif pour le client) + jours_travail_reel: joursTravailReel || null, + nb_representations_reel: nbRepresentationsReel || null, + nb_services_repetitions_reel: nbServicesRepetitionsReel || null, + nb_heures_repetitions_reel: nbHeuresRepetitionsReel || null, + nb_heures_annexes_reel: nbHeuresAnnexesReel || null, + nb_cachets_aem_reel: nbCachetsAemReel || null, + nb_heures_aem_reel: nbHeuresAemReel || null, }; console.log("💾 États actuels avant sauvegarde:", { @@ -1874,15 +1906,6 @@ export default function ContractEditor({
- - +
+
{/* Card 1: États et Statuts - Le plus important */} @@ -2612,6 +2647,15 @@ export default function ContractEditor({ placeholder="ex : 8" />
+
+ +