espace-paie-odentas/components/staff/amendments/AmendmentDureeForm.tsx
odentas 5b72941777 feat: Système complet de gestion des avenants avec signatures électroniques
 Nouvelles fonctionnalités
- Page de gestion des avenants (/staff/avenants)
- Page de détail d'un avenant (/staff/avenants/[id])
- Création d'avenants (objet, durée, rémunération)
- Génération automatique de PDF d'avenant
- Signature électronique via DocuSeal (employeur puis salarié)
- Changement manuel du statut d'un avenant
- Suppression d'avenants

🔧 Routes API
- POST /api/staff/amendments/create - Créer un avenant
- POST /api/staff/amendments/generate-pdf - Générer le PDF
- POST /api/staff/amendments/[id]/send-signature - Envoyer en signature
- POST /api/staff/amendments/[id]/change-status - Changer le statut
- POST /api/webhooks/docuseal-amendment - Webhook après signature employeur
- GET /api/signatures-electroniques/avenants - Liste des avenants en signature

📧 Système email universel v2
- Migration vers le système universel v2 pour les emails d'avenants
- Template 'signature-request-employee-amendment' pour salariés
- Insertion automatique dans DynamoDB pour la Lambda
- Mise à jour automatique du statut dans Supabase

🗄️ Base de données
- Table 'avenants' avec tous les champs (objet, durée, rémunération)
- Colonnes de notification (last_employer_notification_at, last_employee_notification_at)
- Liaison avec cddu_contracts

🎨 Composants
- AvenantDetailPageClient - Détail complet d'un avenant
- ChangeStatusModal - Changement de statut manuel
- SendSignatureModal - Envoi en signature
- DeleteAvenantModal - Suppression avec confirmation
- AvenantSuccessModal - Confirmation de création

📚 Documentation
- AVENANT_EMAIL_SYSTEM_MIGRATION.md - Guide complet de migration

🐛 Corrections
- Fix parsing défensif dans Lambda AWS
- Fix récupération des données depuis DynamoDB
- Fix statut MFA !== 'verified' au lieu de === 'unverified'
2025-10-23 15:30:11 +02:00

319 lines
11 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Calendar } from "lucide-react";
import { OriginalContractData } from "@/types/amendments";
import DatePickerCalendar from "@/components/DatePickerCalendar";
import DatesQuantityModal from "@/components/DatesQuantityModal";
interface AmendmentDureeFormProps {
originalData: OriginalContractData;
data: any;
onChange: (data: any) => void;
}
export default function AmendmentDureeForm({
originalData,
data,
onChange,
}: AmendmentDureeFormProps) {
// Déterminer si c'est un contrat artiste ou technicien
const isTechnician = originalData.categorie_pro === "Technicien";
const isArtist = !isTechnician;
// États pour les calendriers (dates de représentations/répétitions/jours de travail uniquement)
const [showDatesRep, setShowDatesRep] = useState(false);
const [showDatesServ, setShowDatesServ] = useState(false);
const [showJoursTravail, setShowJoursTravail] = useState(false);
// Modal de quantités
const [quantityModalOpen, setQuantityModalOpen] = useState(false);
const [quantityModalType, setQuantityModalType] = useState<
"representations" | "repetitions" | "jours_travail"
>("representations");
const [pendingDates, setPendingDates] = useState<string[]>([]);
const formatDate = (dateStr?: string) => {
if (!dateStr) return "-";
const [y, m, d] = dateStr.split("-");
return `${d}/${m}/${y}`;
};
// Handlers pour les dates simples (début/fin)
const handleDateDebutChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({ ...data, date_debut: e.target.value });
};
const handleDateFinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({ ...data, date_fin: e.target.value });
};
const handleDatesRepApply = (result: {
selectedDates: string[];
hasMultiMonth: boolean;
pdfFormatted: string;
}) => {
setPendingDates(result.selectedDates);
setQuantityModalType("representations");
setQuantityModalOpen(true);
};
const handleDatesServApply = (result: {
selectedDates: string[];
hasMultiMonth: boolean;
pdfFormatted: string;
}) => {
setPendingDates(result.selectedDates);
setQuantityModalType("repetitions");
setQuantityModalOpen(true);
};
const handleJoursTravailApply = (result: {
selectedDates: string[];
hasMultiMonth: boolean;
pdfFormatted: string;
}) => {
// Pour les jours de travail, on n'utilise pas le modal de quantités
// On génère directement la liste des dates au format PDF
onChange({
...data,
jours_travail: result.pdfFormatted,
});
setShowJoursTravail(false);
};
// Handler pour la modal de quantités
const handleQuantityApply = (result: {
selectedDates: string[];
hasMultiMonth: boolean;
pdfFormatted: string;
}) => {
const nbDates = result.selectedDates.length;
switch (quantityModalType) {
case "representations":
onChange({
...data,
dates_representations: result.pdfFormatted,
nb_representations: nbDates,
});
break;
case "repetitions":
onChange({
...data,
dates_repetitions: result.pdfFormatted,
nb_repetitions: nbDates,
});
break;
}
setQuantityModalOpen(false);
};
return (
<div className="bg-orange-50/50 rounded-lg p-4 border border-orange-200 space-y-4">
<h3 className="font-medium text-slate-900 text-sm">Modification de la durée de l'engagement</h3>
{/* Dates de début et fin */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-slate-700 mb-2">
Date de début
</label>
<div className="text-xs text-slate-500 mb-2">
Actuellement : {formatDate(originalData.start_date)}
</div>
<input
type="date"
value={data.date_debut || ""}
onChange={handleDateDebutChange}
className="w-full px-3 py-2 border rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-700 mb-2">
Date de fin
</label>
<div className="text-xs text-slate-500 mb-2">
Actuellement : {formatDate(originalData.end_date)}
</div>
<input
type="date"
value={data.date_fin || ""}
onChange={handleDateFinChange}
className="w-full px-3 py-2 border rounded-lg text-sm"
/>
</div>
</div>
{/* Pour les artistes : représentations et répétitions */}
{isArtist && (
<>
<div>
<label className="block text-xs font-medium text-slate-700 mb-2">
Nombre de représentations
</label>
<div className="text-xs text-slate-500 mb-2">
Actuellement : {originalData.nb_representations || "-"}
</div>
<div className="flex gap-2">
<input
type="number"
min="0"
value={data.nb_representations}
onChange={(e) =>
onChange({
...data,
nb_representations: e.target.value ? Number(e.target.value) : "",
})
}
className="flex-1 px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button
onClick={() => setShowDatesRep(!showDatesRep)}
className="px-3 py-2 border rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2"
>
<Calendar className="h-4 w-4" />
Dates
</button>
</div>
{showDatesRep && (
<DatePickerCalendar
isOpen={showDatesRep}
onClose={() => setShowDatesRep(false)}
onApply={handleDatesRepApply}
initialDates={[]}
title="Sélectionner les dates de représentation"
/>
)}
{data.dates_representations && (
<div className="mt-2 p-2 bg-white rounded text-xs text-slate-600 border">
{data.dates_representations}
</div>
)}
</div>
<div>
<label className="block text-xs font-medium text-slate-700 mb-2">
Nombre de services de répétition
</label>
<div className="text-xs text-slate-500 mb-2">
Actuellement : {originalData.nb_repetitions || "-"}
</div>
<div className="flex gap-2">
<input
type="number"
min="0"
value={data.nb_repetitions}
onChange={(e) =>
onChange({
...data,
nb_repetitions: e.target.value ? Number(e.target.value) : "",
})
}
className="flex-1 px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
/>
<button
onClick={() => setShowDatesServ(!showDatesServ)}
className="px-3 py-2 border rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2"
>
<Calendar className="h-4 w-4" />
Dates
</button>
</div>
{showDatesServ && (
<DatePickerCalendar
isOpen={showDatesServ}
onClose={() => setShowDatesServ(false)}
onApply={handleDatesServApply}
initialDates={[]}
title="Sélectionner les dates de répétition"
/>
)}
{data.dates_repetitions && (
<div className="mt-2 p-2 bg-white rounded text-xs text-slate-600 border">
{data.dates_repetitions}
</div>
)}
</div>
</>
)}
{/* Pour les techniciens : heures et jours de travail */}
{isTechnician && (
<>
<div>
<label className="block text-xs font-medium text-slate-700 mb-2">
Nombre d'heures (global)
</label>
<div className="text-xs text-slate-500 mb-2">
Actuellement : {originalData.nb_heures || "-"}
</div>
<input
type="number"
min="0"
step="0.5"
value={data.nb_heures}
onChange={(e) =>
onChange({
...data,
nb_heures: e.target.value ? Number(e.target.value) : "",
})
}
className="w-full px-3 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
placeholder="ex: 35"
/>
<p className="text-xs text-slate-500 mt-1">
Nombre d'heures total sur la période (sans détail par jour)
</p>
</div>
<div>
<label className="block text-xs font-medium text-slate-700 mb-2">
Jours de travail
</label>
<div className="text-xs text-slate-500 mb-2">
Actuellement : {originalData.jours_travail || "-"}
</div>
<button
onClick={() => setShowJoursTravail(!showJoursTravail)}
className="w-full px-3 py-2 border rounded-lg text-sm hover:bg-slate-50 flex items-center gap-2"
>
<Calendar className="h-4 w-4" />
Sélectionner les jours
</button>
{showJoursTravail && (
<DatePickerCalendar
isOpen={showJoursTravail}
onClose={() => setShowJoursTravail(false)}
onApply={handleJoursTravailApply}
initialDates={[]}
title="Sélectionner les jours de travail"
/>
)}
{data.jours_travail && (
<div className="mt-2 p-2 bg-white rounded text-xs text-slate-600 border">
{data.jours_travail}
</div>
)}
<p className="text-xs text-slate-500 mt-1">
Liste des jours travaillés (sans précision d'heures par jour)
</p>
</div>
</>
)}
{/* Modal de quantités */}
{quantityModalOpen && (
<DatesQuantityModal
isOpen={quantityModalOpen}
onClose={() => setQuantityModalOpen(false)}
selectedDates={pendingDates}
dateType={quantityModalType}
onApply={handleQuantityApply}
/>
)}
</div>
);
}