- 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
197 lines
7.5 KiB
TypeScript
197 lines
7.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { createSbServer } from "@/lib/supabaseServer";
|
|
|
|
async function assertStaff(sb: ReturnType<typeof createSbServer>, userId: string) {
|
|
const { data: me } = await sb
|
|
.from("staff_users")
|
|
.select("is_staff")
|
|
.eq("user_id", userId)
|
|
.maybeSingle();
|
|
return !!me?.is_staff;
|
|
}
|
|
|
|
function sanitizeDate(d: any) {
|
|
if (!d) return null;
|
|
const s = String(d);
|
|
const val = s.length >= 10 ? s.slice(0, 10) : s;
|
|
return /\d{4}-\d{2}-\d{2}/.test(val) ? val : null;
|
|
}
|
|
|
|
// Helper function to parse monetary amounts with French decimal notation (comma)
|
|
function parseMonetaryAmount(value: any): number | null {
|
|
if (value === null || value === undefined || value === "") return null;
|
|
const str = String(value).trim();
|
|
if (!str) return null;
|
|
|
|
// Replace comma with dot for proper parseFloat
|
|
const normalized = str.replace(',', '.');
|
|
const parsed = parseFloat(normalized);
|
|
|
|
return isNaN(parsed) ? null : parsed;
|
|
}
|
|
|
|
// Mapping des noms de champs frontend -> Supabase
|
|
const FIELD_MAPPING: Record<string, string> = {
|
|
"dates_repetitions": "jours_repetitions",
|
|
"nb_representations": "cachets_representations",
|
|
"nb_services_repetition": "services_repetitions",
|
|
"dates_representations": "jours_representations",
|
|
"heures_total": "nombre_d_heures",
|
|
"panier_repas": "paniers_repas"
|
|
// Retirer le mapping brut -> gross_pay
|
|
};
|
|
|
|
const CONTRACT_UPDATABLE_FIELDS = new Set([
|
|
// Champs de base
|
|
"role", "start_date", "end_date", "hours", "gross_pay", "net_pay", "notes",
|
|
"contract_pdf_s3_key", "reference", "structure",
|
|
// Champs de workflow
|
|
"etat_de_la_demande", "contrat_signe", "dpae", "etat_de_la_paie", "aem",
|
|
// Informations contrat
|
|
"production_name", "employee_name", "employee_matricule", "categorie_pro", "profession",
|
|
"objet_spectacle", "contract_number", "analytique",
|
|
// Champs CDDU spécifiques (noms exacts de Supabase)
|
|
"salaire", "type_salaire", "cachets_representations", "services_repetitions",
|
|
"nombre_d_heures", "jours_representations", "jours_repetitions",
|
|
"jours_travail_non_artiste", "jours_travail", "paniers_repas", "nombre_paniers_repas",
|
|
"n_objet", "type_de_contrat", "type_d_embauche", "date_de_reception_de_la_demande",
|
|
"net", "net_apres_pas", "brut", "cout_employeur", "fin_reelle", "motif_fin_contrat",
|
|
"contrat_signe_par_employeur", "nombre_d_heures_par_jour", "precisions_salaire",
|
|
"autreprecision_duree", "autreprecision_salaire",
|
|
"heure_traitement_demande", "cachets", "heures_de_repet", "heures_annexe_8",
|
|
"date_signature_contrat_salarie", "multi_mois", "panier_repas_ccn",
|
|
"si_non_montant_par_panier", "date_signature_contrat_employeur", "virement_effectue",
|
|
"indemnite_hebergement", "nombre_indemnite_hebergement", "indemnite_hebergement_ccn",
|
|
"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",
|
|
// 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 } }) {
|
|
const supabase = createSbServer();
|
|
const { data: { user }, error: authError } = await supabase.auth.getUser();
|
|
if (authError || !user) {
|
|
return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
|
|
}
|
|
const isStaff = await assertStaff(supabase, user.id);
|
|
if (!isStaff) {
|
|
return NextResponse.json({ error: "Accès réservé au staff." }, { status: 403 });
|
|
}
|
|
|
|
const { data: contract, error: cErr } = await supabase
|
|
.from("cddu_contracts")
|
|
.select("*")
|
|
.eq("id", params.id)
|
|
.single();
|
|
|
|
if (cErr || !contract) {
|
|
return NextResponse.json({ error: cErr?.message ?? "Contrat introuvable." }, { status: 404 });
|
|
}
|
|
|
|
const { data: payslips } = await supabase
|
|
.from("payslips")
|
|
.select("*")
|
|
.eq("contract_id", params.id)
|
|
.order("pay_number", { ascending: true });
|
|
|
|
return NextResponse.json({ contract, payslips: payslips ?? [] });
|
|
}
|
|
|
|
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
|
|
const supabase = createSbServer();
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
|
|
const isStaff = await assertStaff(supabase, user.id);
|
|
if (!isStaff) return NextResponse.json({ error: "Accès réservé au staff." }, { status: 403 });
|
|
|
|
const body = await req.json().catch(() => ({}));
|
|
|
|
// Logging pour debug
|
|
console.log("PATCH body received:", JSON.stringify(body, null, 2));
|
|
|
|
if (body?.contract && typeof body.contract === "object") {
|
|
const patch: Record<string, any> = {};
|
|
const skippedFields: string[] = [];
|
|
|
|
for (const k of Object.keys(body.contract)) {
|
|
// Mapper le nom du champ frontend vers le nom Supabase
|
|
const supabaseFieldName = FIELD_MAPPING[k] || k;
|
|
|
|
if (!CONTRACT_UPDATABLE_FIELDS.has(supabaseFieldName)) {
|
|
skippedFields.push(k);
|
|
continue;
|
|
}
|
|
let v = body.contract[k];
|
|
|
|
// Nettoyer les valeurs vides pour les champs numériques
|
|
if (v === "" || v === "null" || v === "undefined") {
|
|
v = null;
|
|
}
|
|
|
|
// Sanitizer les dates
|
|
if (k === "start_date" || k === "end_date") {
|
|
v = sanitizeDate(v);
|
|
}
|
|
|
|
// Normaliser les champs monétaires (gérer les virgules françaises)
|
|
if (["gross_pay", "net_pay", "brut", "net", "net_apres_pas", "cout_employeur", "salaire", "paniers_repas", "panier_repas"].includes(supabaseFieldName)) {
|
|
v = parseMonetaryAmount(v);
|
|
}
|
|
|
|
// Utiliser le nom Supabase dans le patch
|
|
patch[supabaseFieldName] = v;
|
|
}
|
|
|
|
// Synchronisation des champs brut et gross_pay pour maintenir la cohérence
|
|
if (patch.gross_pay !== undefined) {
|
|
// Si gross_pay est mis à jour (champ "Brut" de l'interface), synchroniser brut seulement si brut n'est pas déjà défini
|
|
if (patch.brut === undefined) {
|
|
patch.brut = patch.gross_pay;
|
|
}
|
|
}
|
|
|
|
console.log("Fields to update:", patch);
|
|
console.log("Skipped fields:", skippedFields);
|
|
|
|
if (Object.keys(patch).length > 0) {
|
|
console.log("About to update Supabase with:", patch);
|
|
console.log("Contract ID:", params.id);
|
|
console.log("User ID:", user.id);
|
|
|
|
const { data, error: uErr, count } = await supabase
|
|
.from("cddu_contracts")
|
|
.update(patch)
|
|
.eq("id", params.id)
|
|
.select("*");
|
|
|
|
if (uErr) {
|
|
console.log("Supabase update error:", uErr);
|
|
return NextResponse.json({ error: uErr.message }, { status: 400 });
|
|
}
|
|
|
|
console.log("Update result - data:", data);
|
|
console.log("Update result - count:", count);
|
|
console.log("Update successful for fields:", Object.keys(patch));
|
|
} else {
|
|
console.log("No valid fields to update");
|
|
}
|
|
}
|
|
|
|
const { data: contract } = await supabase
|
|
.from("cddu_contracts").select("*").eq("id", params.id).single();
|
|
|
|
const { data: payslips } = await supabase
|
|
.from("payslips")
|
|
.select("*")
|
|
.eq("contract_id", params.id)
|
|
.order("pay_number", { ascending: true });
|
|
|
|
return NextResponse.json({ contract, payslips: payslips ?? [] });
|
|
}
|