espace-paie-odentas/components/staff/ContractSlide.tsx
2025-10-12 17:05:46 +02:00

194 lines
8.6 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { supabase } from "@/lib/supabaseClient";
type Contract = {
id: string;
contract_number?: string | null;
employee_name?: string | null;
structure?: string | null;
type_de_contrat?: string | null;
start_date?: string | null;
end_date?: string | null;
brut?: string | null;
notes?: string | null;
created_at?: string | null;
// common extra fields
reference?: string | null;
employee_matricule?: string | null;
production_name?: string | null;
gross_pay?: string | null;
net_pay?: string | null;
contract_pdf_s3_key?: string | null;
payslips_count?: number | null;
etat_de_la_demande?: string | null;
contrat_signe?: string | null;
dpae?: string | null;
etat_de_la_paie?: string | null;
aem?: string | null;
// possible aliases / alternate column names present in data
salaries?: string | null;
matricule?: string | null;
production?: string | null;
debut_contrat?: string | null;
fin_contrat?: string | null;
contrat_signe_par_employeur?: string | null;
contrat_pdf?: string | null;
bulletin_paie?: string | null;
};
export default function ContractSlide({ row, onClose, onSave }: { row: Contract | null; onClose: () => void; onSave: (id: string, updates: Partial<Contract>) => void; }) {
const [local, setLocal] = useState<Contract | null>(row);
const timersRef = useRef<Record<string, number>>({});
const [saveStatus, setSaveStatus] = useState<Record<string, 'idle'|'saving'|'saved'|'error'>>({});
// sync when row changes
if (row && (!local || row.id !== local.id)) {
setLocal(row);
}
if (!row) return null;
async function save() {
if (!local) return;
try {
const res = await fetch(`/api/staff/contracts/update`, { method: "POST", body: JSON.stringify(local), headers: { "Content-Type": "application/json" } });
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j?.error || "Update failed");
}
onSave(local.id, local);
onClose();
} catch (err) {
console.error(err);
alert("Échec de la sauvegarde");
}
}
// Save single field to server (immediate)
async function saveField(field: string, value: any) {
if (!local) return;
// clear any pending timer for this field
try { if (timersRef.current[field]) { clearTimeout(timersRef.current[field]); delete timersRef.current[field]; } } catch (e) {}
setSaveStatus((s) => ({ ...s, [field]: 'saving' }));
try {
const body: any = { id: local.id };
body[field] = value;
const res = await fetch(`/api/staff/contracts/update`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j?.error || 'Save failed');
}
// optimistic update to parent grid
onSave(local.id, { [field]: value } as Partial<Contract>);
setSaveStatus((s) => ({ ...s, [field]: 'saved' }));
// reset saved state after short delay
setTimeout(() => setSaveStatus((s) => ({ ...s, [field]: 'idle' })), 1200);
} catch (err) {
console.error('saveField error', field, err);
setSaveStatus((s) => ({ ...s, [field]: 'error' }));
}
}
// Debounced schedule save for a field
function scheduleSaveField(field: string, value: any, delay = 700) {
try { if (timersRef.current[field]) clearTimeout(timersRef.current[field]); } catch (e) {}
const id = window.setTimeout(() => saveField(field, value), delay);
timersRef.current[field] = id;
}
return (
<div className="fixed right-4 top-16 w-[min(900px,80vw)] h-[calc(100vh-4rem)] bg-white border rounded-lg shadow-lg p-4 overflow-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">Détails contrat</h3>
<div className="flex items-center gap-2">
<button className="px-3 py-1 rounded border" onClick={onClose}>Fermer</button>
<button className="px-3 py-1 rounded bg-emerald-600 text-white" onClick={save}>Enregistrer</button>
</div>
</div>
<div className="space-y-4">
{/* Render all available fields dynamically, but prefer a defined order */}
{(() => {
if (!local) return null;
const preferred = [
'contract_number','reference','employee_name','employee_matricule','structure','type_de_contrat','production_name',
'start_date','end_date','brut','net_pay','etat_de_la_demande','contrat_signe','contrat_signe_par_employeur','dpae','aem',
'payslips_count','contract_pdf_s3_key','contrat_pdf','bulletin_paie','notes','created_at'
];
const keys = Array.from(new Set([...(preferred.filter((k) => (local as any)[k] !== undefined && (local as any)[k] !== null)), ...Object.keys(local)]));
return keys.map((key) => {
const rawVal = (local as any)[key];
const value = rawVal === null || rawVal === undefined ? '' : String(rawVal);
const isReadonly = key === 'id' || key === 'created_at';
// guess input type
const lower = key.toLowerCase();
let inputType: 'text'|'date'|'number'|'checkbox'|'textarea' = 'text';
if (lower.includes('date') || lower.includes('debut') || lower.includes('fin')) inputType = 'date';
else if (lower.includes('brut') || lower.includes('pay') || lower.includes('net') || lower.includes('montant') || lower.includes('salaire') || lower === 'payslips_count') inputType = 'number';
else if (lower.includes('note') || value.includes('\n')) inputType = 'textarea';
else if (lower.includes('signe') || lower.includes('signed') || lower.startsWith('is_') || value === 'true' || value === 'false') inputType = 'checkbox';
const label = key.replace(/_/g, ' ').replace(/\b([a-z])/g, (m) => m.toUpperCase());
return (
<div key={key}>
<label className="text-xs text-slate-600 flex items-center justify-between">
<span>{label}</span>
<span className="text-xs text-slate-400">{saveStatus[key] === 'saving' ? '…' : saveStatus[key] === 'saved' ? '✓' : saveStatus[key] === 'error' ? '⚠' : ''}</span>
</label>
<div className="mt-1">
{isReadonly ? (
<div className="rounded border px-2 py-1 text-sm bg-slate-50">{value || '—'}</div>
) : inputType === 'textarea' ? (
<textarea
className="w-full rounded border px-2 py-1 text-sm"
value={value}
onChange={(e) => {
const v = e.target.value;
setLocal((l) => l ? ({ ...l, [key]: v } as Contract) : l);
scheduleSaveField(key, v);
}}
onBlur={(e) => saveField(key, e.target.value)}
/>
) : inputType === 'checkbox' ? (
<input
type="checkbox"
checked={value === 'true' || value === '1' || value === 'on'}
onChange={(e) => {
const v = e.target.checked;
setLocal((l) => l ? ({ ...l, [key]: v } as Contract) : l);
saveField(key, v);
}}
/>
) : (
<input
type={inputType === 'number' ? 'number' : (inputType === 'date' ? 'date' : 'text')}
className="w-full rounded border px-2 py-1 text-sm"
value={value}
onChange={(e) => {
const v = e.target.value;
setLocal((l) => l ? ({ ...l, [key]: v } as Contract) : l);
scheduleSaveField(key, inputType === 'number' ? (v === '' ? null : Number(v)) : v);
}}
onBlur={(e) => {
const v = (e.target as HTMLInputElement).value;
saveField(key, inputType === 'number' ? (v === '' ? null : Number(v)) : v);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
(e.target as HTMLInputElement).blur();
}
}}
/>
)}
</div>
</div>
);
});
})()}
</div>
</div>
);
}