✨ 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'
319 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|