✨ 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'
229 lines
8.7 KiB
TypeScript
229 lines
8.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { FileText, Plus, Search, Check, X } from "lucide-react";
|
|
import { Amendment } from "@/types/amendments";
|
|
|
|
interface StaffAvenantsPageClientProps {
|
|
initialData: Amendment[];
|
|
}
|
|
|
|
export default function StaffAvenantsPageClient({ initialData }: StaffAvenantsPageClientProps) {
|
|
const router = useRouter();
|
|
const [amendments, setAmendments] = useState<Amendment[]>(initialData);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
const filteredAmendments = amendments.filter((amendment) => {
|
|
const term = searchTerm.toLowerCase();
|
|
return (
|
|
amendment.contract_number?.toLowerCase().includes(term) ||
|
|
amendment.employee_name?.toLowerCase().includes(term) ||
|
|
amendment.organization_name?.toLowerCase().includes(term)
|
|
);
|
|
});
|
|
|
|
const formatDate = (dateString?: string) => {
|
|
if (!dateString) return "-";
|
|
const [y, m, d] = dateString.split("-");
|
|
return `${d}/${m}/${y}`;
|
|
};
|
|
|
|
const getStatusBadge = (status: Amendment["status"]) => {
|
|
const badges = {
|
|
draft: "bg-slate-100 text-slate-700",
|
|
pending: "bg-orange-100 text-orange-700",
|
|
signed: "bg-green-100 text-green-700",
|
|
cancelled: "bg-red-100 text-red-700",
|
|
};
|
|
const labels = {
|
|
draft: "Brouillon",
|
|
pending: "En attente",
|
|
signed: "Signé",
|
|
cancelled: "Annulé",
|
|
};
|
|
return (
|
|
<span className={`px-2 py-1 text-xs font-medium rounded ${badges[status]}`}>
|
|
{labels[status]}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
const getSignatureIcons = (signatureStatus?: string) => {
|
|
// Déterminer si employeur a signé
|
|
const employerSigned = signatureStatus === 'pending_employee' || signatureStatus === 'signed';
|
|
// Déterminer si salarié a signé
|
|
const employeeSigned = signatureStatus === 'signed';
|
|
// Si pas encore envoyé
|
|
const notSent = !signatureStatus || signatureStatus === 'not_sent';
|
|
|
|
return (
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-xs font-semibold text-slate-600">E</div>
|
|
{notSent ? (
|
|
<span className="text-xs text-slate-400">—</span>
|
|
) : employerSigned ? (
|
|
<Check className="w-4 h-4 text-green-600" strokeWidth={3} />
|
|
) : (
|
|
<X className="w-4 h-4 text-red-600" strokeWidth={3} />
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="text-xs font-semibold text-slate-600">S</div>
|
|
{notSent ? (
|
|
<span className="text-xs text-slate-400">—</span>
|
|
) : employeeSigned ? (
|
|
<Check className="w-4 h-4 text-green-600" strokeWidth={3} />
|
|
) : (
|
|
<X className="w-4 h-4 text-red-600" strokeWidth={3} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const getElementsLabel = (elements: Amendment["elements"]) => {
|
|
const labels = {
|
|
objet: "Objet",
|
|
duree: "Durée",
|
|
lieu_horaire: "Lieu/Horaire",
|
|
remuneration: "Rémunération",
|
|
};
|
|
return elements.map((el) => labels[el]).join(", ");
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900">Avenants aux contrats</h1>
|
|
<p className="text-sm text-slate-600 mt-1">
|
|
Gérez les avenants aux contrats de travail
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => router.push("/staff/avenants/nouveau")}
|
|
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors shadow-sm"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Nouvel avenant
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search bar */}
|
|
<div className="bg-white rounded-xl border shadow-sm p-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Rechercher par n° contrat, salarié, organisation..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
{filteredAmendments.length === 0 ? (
|
|
<div className="bg-white rounded-xl border shadow-sm p-12 text-center">
|
|
<FileText className="h-12 w-12 text-slate-300 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-slate-900 mb-2">
|
|
{searchTerm ? "Aucun résultat" : "Aucun avenant"}
|
|
</h3>
|
|
<p className="text-sm text-slate-600 mb-6">
|
|
{searchTerm
|
|
? "Aucun avenant ne correspond à votre recherche."
|
|
: "Commencez par créer un nouvel avenant."}
|
|
</p>
|
|
{!searchTerm && (
|
|
<button
|
|
onClick={() => router.push("/staff/avenants/nouveau")}
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
Créer le premier avenant
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="bg-white rounded-xl border shadow-sm overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-slate-50 border-b">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
N° Contrat
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
Salarié
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
Organisation
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
Éléments avenantés
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
Date d'effet
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
Signé
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
Statut
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-slate-600">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y">
|
|
{filteredAmendments.map((amendment) => (
|
|
<tr
|
|
key={amendment.id}
|
|
onClick={() => router.push(`/staff/avenants/${amendment.id}`)}
|
|
className="hover:bg-slate-50 transition-colors cursor-pointer"
|
|
>
|
|
<td className="px-4 py-3 text-sm font-medium text-slate-900">
|
|
{amendment.contract_number || "-"}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-700">
|
|
{amendment.employee_name || "-"}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-700">
|
|
{amendment.organization_name || "-"}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-700">
|
|
{getElementsLabel(amendment.elements)}
|
|
</td>
|
|
<td className="px-4 py-3 text-sm text-slate-700">
|
|
{formatDate(amendment.date_effet)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{getSignatureIcons(amendment.signature_status)}
|
|
</td>
|
|
<td className="px-4 py-3">{getStatusBadge(amendment.status)}</td>
|
|
<td className="px-4 py-3 text-sm">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
router.push(`/staff/avenants/${amendment.id}`);
|
|
}}
|
|
className="text-indigo-600 hover:text-indigo-700 font-medium"
|
|
>
|
|
Voir
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|