feat: Ajouter interface de saisie en masse du temps de travail réel
- Création de la page /staff/contrats/saisie-temps-reel avec tableau éditable - Ajout des colonnes jours_representations et jours_repetitions dans l'API - Construction intelligente du TT Contractuel (concaténation des sources) - Ajout de la colonne temps_reel_traite pour marquer les contrats traités - Interface avec filtres (année, mois, organisation, recherche) - Tri par date/salarié - Édition inline avec auto-save via API - Checkbox pour marquer comme traité (masque automatiquement la ligne) - Toggle pour afficher/masquer les contrats traités - Migration SQL pour la colonne temps_reel_traite - Ajout du menu 'Temps de travail réel' dans la sidebar - Logs de débogage pour le suivi des sauvegardes
This commit is contained in:
parent
7fae87353c
commit
965b1fb9cd
10 changed files with 1266 additions and 28 deletions
108
TEMPS_TRAVAIL_REEL.md
Normal file
108
TEMPS_TRAVAIL_REEL.md
Normal file
|
|
@ -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
|
||||||
|
<Section title="Temps de travail réel" icon={Calendar}>
|
||||||
|
<Field label="Jours travaillés" value={data.jours_travail_reel} />
|
||||||
|
<Field label="Nbre de représentations" value={data.nb_representations_reel} />
|
||||||
|
// etc.
|
||||||
|
</Section>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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é
|
||||||
|
|
@ -115,7 +115,7 @@ function usePayslips(contractId: string) {
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { api } from "@/lib/fetcher";
|
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { NotesSection } from "@/components/NotesSection";
|
import { NotesSection } from "@/components/NotesSection";
|
||||||
|
|
@ -1563,20 +1563,139 @@ return (
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Temps de travail réel" icon={Calendar}>
|
<Section title="Temps de travail réel" icon={Calendar}>
|
||||||
<Field
|
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-6 border border-blue-100">
|
||||||
label="Jours travaillés"
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-5">
|
||||||
value={
|
{/* Jours travaillés */}
|
||||||
data.categorie_prof === "Technicien" && data.jours_travail_non_artiste
|
<div className="md:col-span-2 pb-4 border-b border-blue-200">
|
||||||
? data.jours_travail_non_artiste
|
<div className="flex items-start gap-3">
|
||||||
: (data.jours_travailles ?? 0)
|
<div className="mt-1 w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||||
}
|
<Calendar className="w-5 h-5 text-blue-600" />
|
||||||
/>
|
</div>
|
||||||
<Field label="Nbre de représentations" value={data.nb_representations ?? 0} />
|
<div className="flex-1">
|
||||||
<Field label="Nbre de services répétitions" value={data.nb_services_repetitions ?? 0} />
|
<div className="text-xs font-medium text-blue-600 uppercase tracking-wide mb-1">
|
||||||
<Field label="Nbre d'heures répétitions" value={data.nb_heures_repetitions ?? 0} />
|
Jours travaillés
|
||||||
<Field label="Nbre d'heures Annexes 8" value={data.nb_heures_annexes ?? 0} />
|
</div>
|
||||||
<Field label="Nombre de cachets AEM" value={data.nb_cachets_aem ?? 0} />
|
<div className="text-base font-semibold text-slate-900">
|
||||||
<Field label="Nombre d'heures AEM" value={data.nb_heures_aem ?? 0} />
|
{data.jours_travail_reel || "—"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Représentations */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<div className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||||
|
Représentations
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">
|
||||||
|
{data.nb_representations_reel ?? 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-1">cachets</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services répétitions */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<div className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||||
|
Services répétitions
|
||||||
|
</div>
|
||||||
|
<div className="group relative">
|
||||||
|
<HelpCircle className="w-3.5 h-3.5 text-slate-400 cursor-help" />
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block w-64 bg-slate-900 text-white text-xs rounded-lg px-3 py-2 shadow-lg z-10">
|
||||||
|
Indique le nombre de services de répétitions réels, indépendamment du type de rémunération (au cachet ou à l'heure).
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">
|
||||||
|
{data.nb_services_repetitions_reel ?? 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-1">services</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Heures répétitions */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<div className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||||
|
Heures répétitions
|
||||||
|
</div>
|
||||||
|
<div className="group relative">
|
||||||
|
<HelpCircle className="w-3.5 h-3.5 text-slate-400 cursor-help" />
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block w-64 bg-slate-900 text-white text-xs rounded-lg px-3 py-2 shadow-lg z-10">
|
||||||
|
Indique le nombre d'heures de répétitions réel, indépendamment du type de rémunération (au cachet ou à l'heure).
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">
|
||||||
|
{data.nb_heures_repetitions_reel ?? 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-1">heures</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Heures Annexe 8 */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<div className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||||
|
Heures Annexe 8
|
||||||
|
</div>
|
||||||
|
<div className="group relative">
|
||||||
|
<HelpCircle className="w-3.5 h-3.5 text-slate-400 cursor-help" />
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block w-64 bg-slate-900 text-white text-xs rounded-lg px-3 py-2 shadow-lg z-10">
|
||||||
|
Concerne uniquement les techniciens (Annexe 8). Les techniciens ne peuvent être rémunérés qu'à l'heure.
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">
|
||||||
|
{data.nb_heures_annexes_reel ?? 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-1">heures</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cachets AEM */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<div className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||||
|
Cachets AEM
|
||||||
|
</div>
|
||||||
|
<div className="group relative">
|
||||||
|
<HelpCircle className="w-3.5 h-3.5 text-slate-400 cursor-help" />
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block w-64 bg-slate-900 text-white text-xs rounded-lg px-3 py-2 shadow-lg z-10">
|
||||||
|
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.
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">
|
||||||
|
{data.nb_cachets_aem_reel ?? 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-1">cachets</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Heures AEM */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
|
<div className="text-xs font-medium text-slate-500 uppercase tracking-wide">
|
||||||
|
Heures AEM
|
||||||
|
</div>
|
||||||
|
<div className="group relative">
|
||||||
|
<HelpCircle className="w-3.5 h-3.5 text-slate-400 cursor-help" />
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block w-64 bg-slate-900 text-white text-xs rounded-lg px-3 py-2 shadow-lg z-10">
|
||||||
|
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).
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-slate-900">
|
||||||
|
{data.nb_heures_aem_reel ?? 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500 mt-1">heures</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
619
app/(staff)/staff/contrats/saisie-temps-reel/page.tsx
Normal file
619
app/(staff)/staff/contrats/saisie-temps-reel/page.tsx
Normal file
|
|
@ -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<string>("all");
|
||||||
|
const [monthFilter, setMonthFilter] = useState<string>("all");
|
||||||
|
const [orgFilter, setOrgFilter] = useState<string>("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<Record<string, any>>({});
|
||||||
|
const [savingCells, setSavingCells] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Récupération des contrats
|
||||||
|
const { data: contracts = [], isLoading, error } = useQuery<Contract[]>({
|
||||||
|
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<Array<{id: string; name: string}>>({
|
||||||
|
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 (
|
||||||
|
<div className="min-h-screen bg-slate-50 p-6">
|
||||||
|
<div className="max-w-[1800px] mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Link href="/staff/contrats">
|
||||||
|
<Button variant="ghost" size="sm" className="mb-4">
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
Retour aux contrats
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-3xl font-bold text-slate-900">Saisie Temps de Travail Réel</h1>
|
||||||
|
<p className="text-slate-600 mt-2">
|
||||||
|
Saisissez rapidement les données réelles pour alimenter la vue client. Cliquez sur une cellule pour modifier, les modifications sont sauvegardées automatiquement.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtres */}
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
|
{/* Recherche */}
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-2 block">Rechercher</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Production, salarié, profession..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Année */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-2 block">Année</label>
|
||||||
|
<select
|
||||||
|
value={yearFilter}
|
||||||
|
onChange={(e) => setYearFilter(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">Toutes</option>
|
||||||
|
{years.map((year) => (
|
||||||
|
<option key={year} value={year}>
|
||||||
|
{year}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mois */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-2 block">Mois</label>
|
||||||
|
<select
|
||||||
|
value={monthFilter}
|
||||||
|
onChange={(e) => setMonthFilter(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg text-sm"
|
||||||
|
disabled={yearFilter === "all"}
|
||||||
|
>
|
||||||
|
<option value="all">Tous</option>
|
||||||
|
<option value="1">Janvier</option>
|
||||||
|
<option value="2">Février</option>
|
||||||
|
<option value="3">Mars</option>
|
||||||
|
<option value="4">Avril</option>
|
||||||
|
<option value="5">Mai</option>
|
||||||
|
<option value="6">Juin</option>
|
||||||
|
<option value="7">Juillet</option>
|
||||||
|
<option value="8">Août</option>
|
||||||
|
<option value="9">Septembre</option>
|
||||||
|
<option value="10">Octobre</option>
|
||||||
|
<option value="11">Novembre</option>
|
||||||
|
<option value="12">Décembre</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Organisation */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-medium text-slate-600 mb-2 block">Organisation</label>
|
||||||
|
<select
|
||||||
|
value={orgFilter}
|
||||||
|
onChange={(e) => setOrgFilter(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<option value="all">Toutes</option>
|
||||||
|
{organizations?.map((org: any) => (
|
||||||
|
<option key={org.id} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tri et affichage des traités */}
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-xs font-medium text-slate-600">Trier par :</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant={sortBy === "date" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (sortBy === "date") {
|
||||||
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
setSortBy("date");
|
||||||
|
setSortOrder("desc");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Date {sortBy === "date" && (sortOrder === "asc" ? "↑" : "↓")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={sortBy === "employee" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (sortBy === "employee") {
|
||||||
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
|
} else {
|
||||||
|
setSortBy("employee");
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Salarié {sortBy === "employee" && (sortOrder === "asc" ? "↑" : "↓")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle pour afficher les traités */}
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showTraites}
|
||||||
|
onChange={(e) => setShowTraites(e.target.checked)}
|
||||||
|
className="w-4 h-4 rounded border-slate-300"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-600">Afficher les contrats traités</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Tableau */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="bg-slate-100 border-b sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-3 text-left font-semibold text-slate-700 w-32">Salarié</th>
|
||||||
|
<th className="px-3 py-3 text-left font-semibold text-slate-700 w-32">Profession</th>
|
||||||
|
<th className="px-3 py-3 text-left font-semibold text-slate-700 w-24">Dates</th>
|
||||||
|
<th className="px-3 py-3 text-left font-semibold text-slate-500 w-48 bg-slate-50">TT Contractuel</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-slate-500 w-20 bg-slate-50">Cach.</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-slate-500 w-20 bg-slate-50">Serv.</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-slate-500 w-20 bg-slate-50">Heures</th>
|
||||||
|
<th className="px-3 py-3 text-left font-semibold text-slate-500 w-56 bg-slate-50">Précisions</th>
|
||||||
|
<th className="px-3 py-3 text-left font-semibold text-blue-700 w-32 bg-blue-50">Jours réels</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-blue-700 w-24 bg-blue-50">Repr.</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-blue-700 w-24 bg-blue-50">Serv. rép.</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-blue-700 w-24 bg-blue-50">H. rép.</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-blue-700 w-24 bg-blue-50">H. Ann.8</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-blue-700 w-24 bg-blue-50">Cach. AEM</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-blue-700 w-24 bg-blue-50">H. AEM</th>
|
||||||
|
<th className="px-3 py-3 text-center font-semibold text-green-700 w-20 bg-green-50">Traité</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={17} className="px-3 py-12 text-center text-slate-400">
|
||||||
|
Chargement des contrats...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : error ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={17} className="px-3 py-12 text-center">
|
||||||
|
<div className="text-red-600 font-medium">Erreur de chargement</div>
|
||||||
|
<div className="text-sm text-slate-500 mt-2">{String(error)}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : filteredContracts.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={17} className="px-3 py-12 text-center">
|
||||||
|
<div className="text-slate-400 mb-2">Aucun contrat trouvé avec ces filtres.</div>
|
||||||
|
<div className="text-xs text-slate-400">Total dans la base : {contracts.length}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
filteredContracts.map((contract) => (
|
||||||
|
<tr key={contract.id} className="border-b hover:bg-slate-50">
|
||||||
|
{/* Salarié */}
|
||||||
|
<td className="px-3 py-2 text-slate-600 text-xs">
|
||||||
|
<div className="truncate" title={contract.employee_name}>
|
||||||
|
{contract.employee_name || "—"}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{/* Profession */}
|
||||||
|
<td className="px-3 py-2 text-slate-600 text-xs">
|
||||||
|
{contract.profession || "—"}
|
||||||
|
</td>
|
||||||
|
{/* Dates */}
|
||||||
|
<td className="px-3 py-2 text-slate-600 text-xs">
|
||||||
|
{contract.start_date ? new Date(contract.start_date).toLocaleDateString("fr-FR") : "—"}
|
||||||
|
</td>
|
||||||
|
{/* TT Contractuel (référence) */}
|
||||||
|
<td className="px-3 py-2 text-slate-500 text-xs bg-slate-50 max-w-[12rem]">
|
||||||
|
<div className="whitespace-normal break-words">
|
||||||
|
{(() => {
|
||||||
|
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(" + ") : "—";
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{/* Cachets représentations (PDFMonkey) */}
|
||||||
|
<td className="px-3 py-2 text-center text-slate-500 text-xs bg-slate-50">
|
||||||
|
{contract.cachets_representations ?? "—"}
|
||||||
|
</td>
|
||||||
|
{/* Services répétitions (PDFMonkey) */}
|
||||||
|
<td className="px-3 py-2 text-center text-slate-500 text-xs bg-slate-50">
|
||||||
|
{contract.services_repetitions ?? "—"}
|
||||||
|
</td>
|
||||||
|
{/* Nombre d'heures (PDFMonkey) */}
|
||||||
|
<td className="px-3 py-2 text-center text-slate-500 text-xs bg-slate-50">
|
||||||
|
{contract.nombre_d_heures ?? "—"}
|
||||||
|
</td>
|
||||||
|
{/* Précisions salaire (référence) */}
|
||||||
|
<td className="px-3 py-2 text-slate-500 text-xs bg-slate-50 max-w-[14rem]">
|
||||||
|
<div className="whitespace-normal break-words">
|
||||||
|
{contract.precisions_salaire || "—"}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{/* Jours travaillés réel */}
|
||||||
|
<td className="px-3 py-2 bg-blue-50">
|
||||||
|
<EditableCell
|
||||||
|
contractId={contract.id}
|
||||||
|
field="jours_travail_reel"
|
||||||
|
value={contract.jours_travail_reel}
|
||||||
|
editedValue={editedCells[`${contract.id}-jours_travail_reel`]}
|
||||||
|
onChange={handleCellChange}
|
||||||
|
onSave={handleCellSave}
|
||||||
|
isSaving={savingCells.has(`${contract.id}-jours_travail_reel`)}
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/* Nb représentations réel */}
|
||||||
|
<td className="px-3 py-2 bg-blue-50">
|
||||||
|
<EditableCell
|
||||||
|
contractId={contract.id}
|
||||||
|
field="nb_representations_reel"
|
||||||
|
value={contract.nb_representations_reel}
|
||||||
|
editedValue={editedCells[`${contract.id}-nb_representations_reel`]}
|
||||||
|
onChange={handleCellChange}
|
||||||
|
onSave={handleCellSave}
|
||||||
|
isSaving={savingCells.has(`${contract.id}-nb_representations_reel`)}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/* Nb services répétitions réel */}
|
||||||
|
<td className="px-3 py-2 bg-blue-50">
|
||||||
|
<EditableCell
|
||||||
|
contractId={contract.id}
|
||||||
|
field="nb_services_repetitions_reel"
|
||||||
|
value={contract.nb_services_repetitions_reel}
|
||||||
|
editedValue={editedCells[`${contract.id}-nb_services_repetitions_reel`]}
|
||||||
|
onChange={handleCellChange}
|
||||||
|
onSave={handleCellSave}
|
||||||
|
isSaving={savingCells.has(`${contract.id}-nb_services_repetitions_reel`)}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/* Nb heures répétitions réel */}
|
||||||
|
<td className="px-3 py-2 bg-blue-50">
|
||||||
|
<EditableCell
|
||||||
|
contractId={contract.id}
|
||||||
|
field="nb_heures_repetitions_reel"
|
||||||
|
value={contract.nb_heures_repetitions_reel}
|
||||||
|
editedValue={editedCells[`${contract.id}-nb_heures_repetitions_reel`]}
|
||||||
|
onChange={handleCellChange}
|
||||||
|
onSave={handleCellSave}
|
||||||
|
isSaving={savingCells.has(`${contract.id}-nb_heures_repetitions_reel`)}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/* Nb heures Annexe 8 réel */}
|
||||||
|
<td className="px-3 py-2 bg-blue-50">
|
||||||
|
<EditableCell
|
||||||
|
contractId={contract.id}
|
||||||
|
field="nb_heures_annexes_reel"
|
||||||
|
value={contract.nb_heures_annexes_reel}
|
||||||
|
editedValue={editedCells[`${contract.id}-nb_heures_annexes_reel`]}
|
||||||
|
onChange={handleCellChange}
|
||||||
|
onSave={handleCellSave}
|
||||||
|
isSaving={savingCells.has(`${contract.id}-nb_heures_annexes_reel`)}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/* Nb cachets AEM réel */}
|
||||||
|
<td className="px-3 py-2 bg-blue-50">
|
||||||
|
<EditableCell
|
||||||
|
contractId={contract.id}
|
||||||
|
field="nb_cachets_aem_reel"
|
||||||
|
value={contract.nb_cachets_aem_reel}
|
||||||
|
editedValue={editedCells[`${contract.id}-nb_cachets_aem_reel`]}
|
||||||
|
onChange={handleCellChange}
|
||||||
|
onSave={handleCellSave}
|
||||||
|
isSaving={savingCells.has(`${contract.id}-nb_cachets_aem_reel`)}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/* Nb heures AEM réel */}
|
||||||
|
<td className="px-3 py-2 bg-blue-50">
|
||||||
|
<EditableCell
|
||||||
|
contractId={contract.id}
|
||||||
|
field="nb_heures_aem_reel"
|
||||||
|
value={contract.nb_heures_aem_reel}
|
||||||
|
editedValue={editedCells[`${contract.id}-nb_heures_aem_reel`]}
|
||||||
|
onChange={handleCellChange}
|
||||||
|
onSave={handleCellSave}
|
||||||
|
isSaving={savingCells.has(`${contract.id}-nb_heures_aem_reel`)}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{/* Checkbox traité */}
|
||||||
|
<td className="px-3 py-2 bg-green-50 text-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={contract.temps_reel_traite || false}
|
||||||
|
onChange={(e) => {
|
||||||
|
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é"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistiques */}
|
||||||
|
{filteredContracts.length > 0 && (
|
||||||
|
<div className="mt-4 text-sm text-slate-600">
|
||||||
|
{filteredContracts.length} contrat{filteredContracts.length > 1 ? "s" : ""} affiché{filteredContracts.length > 1 ? "s" : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
value={displayValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 && (
|
||||||
|
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||||||
|
<div className="w-3 h-3 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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_heures_annexes: cddu.heures_annexe_8 ? Number(cddu.heures_annexe_8) : undefined,
|
||||||
nb_cachets_aem: cddu.cachets ? Number(cddu.cachets) : undefined,
|
nb_cachets_aem: cddu.cachets ? Number(cddu.cachets) : undefined,
|
||||||
nb_heures_aem: cddu.nombre_d_heures ? Number(cddu.nombre_d_heures) : 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,
|
created_at: cddu.created_at,
|
||||||
updated_at: cddu.updated_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) {
|
if (requestBody.jours_travail_non_artiste !== undefined) {
|
||||||
supabaseData.jours_travail_non_artiste = requestBody.jours_travail_non_artiste;
|
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) {
|
if (requestBody.type_salaire !== undefined) {
|
||||||
supabaseData.type_salaire = requestBody.type_salaire;
|
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',
|
'cachets_representations', 'services_repetitions', 'jours_representations',
|
||||||
'jours_repetitions', 'nombre_d_heures', 'jours_travail',
|
'jours_repetitions', 'nombre_d_heures', 'jours_travail',
|
||||||
'jours_travail_non_artiste', 'notes',
|
'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 => {
|
fieldsToSync.forEach(field => {
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,13 @@ const CONTRACT_UPDATABLE_FIELDS = new Set([
|
||||||
"si_non_montant_par_indemnite", "mineur_entre_16_et_18", "civilite_representant_legal",
|
"si_non_montant_par_indemnite", "mineur_entre_16_et_18", "civilite_representant_legal",
|
||||||
"nom_representant_legal", "adresse_representant_legal", "dob_representant_legal",
|
"nom_representant_legal", "adresse_representant_legal", "dob_representant_legal",
|
||||||
"cob_representant_legal", "periode", "docuseal_template_id", "docuseal_submission_id",
|
"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 } }) {
|
export async function GET(_req: Request, { params }: { params: { id: string } }) {
|
||||||
|
|
|
||||||
166
app/api/staff/contrats/temps-reel/route.ts
Normal file
166
app/api/staff/contrats/temps-reel/route.ts
Normal file
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState, useEffect, useRef } from "react";
|
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 { api } from "@/lib/fetcher";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import LogoutButton from "@/components/LogoutButton";
|
import LogoutButton from "@/components/LogoutButton";
|
||||||
|
|
@ -558,6 +558,14 @@ export default function Sidebar({ clientInfo, isStaff = false, mobile = false, o
|
||||||
<span>Contrats</span>
|
<span>Contrats</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/staff/contrats/saisie-temps-reel" onClick={() => 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">
|
||||||
|
<span className="inline-flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4" aria-hidden />
|
||||||
|
<span>Temps de travail réel</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
<Link href="/staff/payslips" onClick={() => onNavigate && onNavigate()} className={`block px-3 py-2 rounded-xl text-sm transition truncate ${
|
<Link href="/staff/payslips" onClick={() => 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"
|
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">
|
}`} title="Gestion des fiches de paie">
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,18 @@ export default function ContractEditor({
|
||||||
const [nombreHeuresTotal, setNombreHeuresTotal] = useState<number | "">(contract.nombre_d_heures || "");
|
const [nombreHeuresTotal, setNombreHeuresTotal] = useState<number | "">(contract.nombre_d_heures || "");
|
||||||
const [nombreHeuresParJour, setNombreHeuresParJour] = useState<number | "">(contract.nombre_d_heures_par_jour || "");
|
const [nombreHeuresParJour, setNombreHeuresParJour] = useState<number | "">(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<number | "">(contract.nb_representations_reel || "");
|
||||||
|
const [datesRepresentationsReel, setDatesRepresentationsReel] = useState(contract.dates_representations_reel || "");
|
||||||
|
const [nbServicesRepetitionsReel, setNbServicesRepetitionsReel] = useState<number | "">(contract.nb_services_repetitions_reel || "");
|
||||||
|
const [nbHeuresRepetitionsReel, setNbHeuresRepetitionsReel] = useState<number | "">(contract.nb_heures_repetitions_reel || "");
|
||||||
|
const [datesRepetitionsReel, setDatesRepetitionsReel] = useState(contract.dates_repetitions_reel || "");
|
||||||
|
const [nbHeuresAnnexesReel, setNbHeuresAnnexesReel] = useState<number | "">(contract.nb_heures_annexes_reel || "");
|
||||||
|
const [nbCachetsAemReel, setNbCachetsAemReel] = useState<number | "">(contract.nb_cachets_aem_reel || "");
|
||||||
|
const [nbHeuresAemReel, setNbHeuresAemReel] = useState<number | "">(contract.nb_heures_aem_reel || "");
|
||||||
|
|
||||||
// Synchroniser les états avec les données du contrat
|
// Synchroniser les états avec les données du contrat
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Calculer le mode (heures vs représentations/répétitions)
|
// Calculer le mode (heures vs représentations/répétitions)
|
||||||
|
|
@ -508,6 +520,18 @@ export default function ContractEditor({
|
||||||
setPrecisionsSalaire(contract.precisions_salaire || "");
|
setPrecisionsSalaire(contract.precisions_salaire || "");
|
||||||
setAutrePrecisionDuree(contract.autreprecision_duree || "");
|
setAutrePrecisionDuree(contract.autreprecision_duree || "");
|
||||||
setAutrePrecisionSalaire(contract.autreprecision_salaire || "");
|
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]);
|
}, [contract, categoriePro, professionPick?.code]);
|
||||||
|
|
||||||
// États pour les autres champs CDDU actuellement grisés
|
// États pour les autres champs CDDU actuellement grisés
|
||||||
|
|
@ -1103,6 +1127,14 @@ export default function ContractEditor({
|
||||||
brut: typeSalaire === "Brut" ? parseMonetaryAmount(montant) : null,
|
brut: typeSalaire === "Brut" ? parseMonetaryAmount(montant) : null,
|
||||||
// Le champ gross_pay correspond au champ "Brut" de l'interface utilisateur
|
// Le champ gross_pay correspond au champ "Brut" de l'interface utilisateur
|
||||||
gross_pay: parseMonetaryAmount(form.gross_pay),
|
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:", {
|
console.log("💾 États actuels avant sauvegarde:", {
|
||||||
|
|
@ -1874,15 +1906,6 @@ export default function ContractEditor({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 justify-end">
|
<div className="flex gap-2 justify-end">
|
||||||
<Button
|
|
||||||
onClick={saveContract}
|
|
||||||
disabled={isSaving}
|
|
||||||
className="rounded-2xl px-5"
|
|
||||||
>
|
|
||||||
<Save className="size-4 mr-2" />
|
|
||||||
{isSaving ? "Sauvegarde..." : "Enregistrer le contrat"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={generatePdf}
|
onClick={generatePdf}
|
||||||
disabled={isGeneratingPdf}
|
disabled={isGeneratingPdf}
|
||||||
|
|
@ -1910,6 +1933,18 @@ export default function ContractEditor({
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* Bouton flottant pour enregistrer */}
|
||||||
|
<div className="fixed bottom-6 right-6 z-50">
|
||||||
|
<Button
|
||||||
|
onClick={saveContract}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="rounded-full shadow-lg hover:shadow-xl transition-all duration-200 px-8 py-7 bg-green-600 hover:bg-green-700 text-black text-base font-semibold ring-2 ring-green-400 ring-offset-2"
|
||||||
|
>
|
||||||
|
<Save className="size-5 mr-2 text-black" />
|
||||||
|
{isSaving ? "Sauvegarde..." : "Enregistrer"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<div className="col-span-2 space-y-6">
|
<div className="col-span-2 space-y-6">
|
||||||
{/* Card 1: États et Statuts - Le plus important */}
|
{/* Card 1: États et Statuts - Le plus important */}
|
||||||
|
|
@ -2612,6 +2647,15 @@ export default function ContractEditor({
|
||||||
placeholder="ex : 8"
|
placeholder="ex : 8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="text-xs text-muted-foreground">Jours de travail</label>
|
||||||
|
<Textarea
|
||||||
|
rows={3}
|
||||||
|
value={joursTravail}
|
||||||
|
onChange={(e) => setJoursTravail(e.target.value)}
|
||||||
|
placeholder="ex : 12/10, 13/10, 24/10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -2640,7 +2684,8 @@ export default function ContractEditor({
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-muted-foreground">Jours de travail</label>
|
<label className="text-xs text-muted-foreground">Jours de travail</label>
|
||||||
<Input
|
<Textarea
|
||||||
|
rows={3}
|
||||||
value={joursTravail}
|
value={joursTravail}
|
||||||
onChange={(e) => setJoursTravail(e.target.value)}
|
onChange={(e) => setJoursTravail(e.target.value)}
|
||||||
placeholder="ex : 12/10, 13/10, 24/10"
|
placeholder="ex : 12/10, 13/10, 24/10"
|
||||||
|
|
@ -2667,6 +2712,95 @@ export default function ContractEditor({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Séparateur */}
|
||||||
|
<Separator className="my-6" />
|
||||||
|
|
||||||
|
{/* Sous-section: Temps de travail réel (informatif pour le client) */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<div className="h-px flex-1 bg-gradient-to-r from-slate-200 to-transparent"></div>
|
||||||
|
<h4 className="text-sm font-semibold text-slate-700">Temps de travail réel (informatif client)</h4>
|
||||||
|
<div className="h-px flex-1 bg-gradient-to-l from-slate-200 to-transparent"></div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Ces informations sont purement informatives pour le client et ne sont pas utilisées pour la génération du PDF du contrat.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">Jours travaillés</label>
|
||||||
|
<Input
|
||||||
|
value={joursTravailReel}
|
||||||
|
onChange={(e) => setJoursTravailReel(e.target.value)}
|
||||||
|
placeholder="ex: 15/01, 16/01, 17/01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">Nbre de représentations</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={nbRepresentationsReel}
|
||||||
|
onChange={(e) => setNbRepresentationsReel(e.target.value === "" ? "" : Number(e.target.value))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">Nbre de services répétitions</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={nbServicesRepetitionsReel}
|
||||||
|
onChange={(e) => setNbServicesRepetitionsReel(e.target.value === "" ? "" : Number(e.target.value))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">Nbre d'heures répétitions</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step="0.5"
|
||||||
|
value={nbHeuresRepetitionsReel}
|
||||||
|
onChange={(e) => setNbHeuresRepetitionsReel(e.target.value === "" ? "" : Number(e.target.value))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">Nbre d'heures Annexes 8</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step="0.5"
|
||||||
|
value={nbHeuresAnnexesReel}
|
||||||
|
onChange={(e) => setNbHeuresAnnexesReel(e.target.value === "" ? "" : Number(e.target.value))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">Nombre de cachets AEM</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={nbCachetsAemReel}
|
||||||
|
onChange={(e) => setNbCachetsAemReel(e.target.value === "" ? "" : Number(e.target.value))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-muted-foreground">Nombre d'heures AEM</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step="0.5"
|
||||||
|
value={nbHeuresAemReel}
|
||||||
|
onChange={(e) => setNbHeuresAemReel(e.target.value === "" ? "" : Number(e.target.value))}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
5
migrations/add_temps_reel_traite_column.sql
Normal file
5
migrations/add_temps_reel_traite_column.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- Ajouter une colonne pour marquer les contrats comme traités pour le temps de travail réel
|
||||||
|
ALTER TABLE cddu_contracts
|
||||||
|
ADD COLUMN IF NOT EXISTS temps_reel_traite BOOLEAN DEFAULT false;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN cddu_contracts.temps_reel_traite IS 'Indique si le temps de travail réel a été saisi et validé';
|
||||||
34
migrations/add_temps_travail_reel_columns.sql
Normal file
34
migrations/add_temps_travail_reel_columns.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- Migration: Ajouter les colonnes pour le temps de travail réel
|
||||||
|
-- Ces colonnes sont purement informatives pour les clients et ne sont pas utilisées pour la génération PDF
|
||||||
|
|
||||||
|
-- Ajouter les colonnes dans cddu_contracts
|
||||||
|
ALTER TABLE cddu_contracts
|
||||||
|
-- Jours travaillés
|
||||||
|
ADD COLUMN IF NOT EXISTS jours_travail_reel TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS jours_travail_non_artiste_reel TEXT,
|
||||||
|
|
||||||
|
-- Représentations
|
||||||
|
ADD COLUMN IF NOT EXISTS nb_representations_reel INTEGER DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS dates_representations_reel TEXT,
|
||||||
|
|
||||||
|
-- Répétitions
|
||||||
|
ADD COLUMN IF NOT EXISTS nb_services_repetitions_reel INTEGER DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS nb_heures_repetitions_reel NUMERIC(10, 2) DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS dates_repetitions_reel TEXT,
|
||||||
|
|
||||||
|
-- Heures annexes et AEM
|
||||||
|
ADD COLUMN IF NOT EXISTS nb_heures_annexes_reel NUMERIC(10, 2) DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS nb_cachets_aem_reel INTEGER DEFAULT 0,
|
||||||
|
ADD COLUMN IF NOT EXISTS nb_heures_aem_reel NUMERIC(10, 2) DEFAULT 0;
|
||||||
|
|
||||||
|
-- Ajouter des commentaires pour documenter les colonnes
|
||||||
|
COMMENT ON COLUMN cddu_contracts.jours_travail_reel IS 'Jours de travail réels effectués (artiste) - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.jours_travail_non_artiste_reel IS 'Jours de travail réels effectués (technicien) - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.nb_representations_reel IS 'Nombre réel de représentations effectuées - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.dates_representations_reel IS 'Dates réelles des représentations - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.nb_services_repetitions_reel IS 'Nombre réel de services de répétition - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.nb_heures_repetitions_reel IS 'Nombre réel d''heures de répétition - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.dates_repetitions_reel IS 'Dates réelles des répétitions - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.nb_heures_annexes_reel IS 'Nombre réel d''heures Annexes 8 - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.nb_cachets_aem_reel IS 'Nombre réel de cachets AEM - Informatif pour client';
|
||||||
|
COMMENT ON COLUMN cddu_contracts.nb_heures_aem_reel IS 'Nombre réel d''heures AEM - Informatif pour client';
|
||||||
Loading…
Reference in a new issue