fix: Utiliser categorie_pro du contrat pour déterminer employee_catpro dans avenants
- Correction de la détection artiste/technicien dans génération PDF avenant - Utilisation de contract.categorie_pro au lieu de déduction depuis type_de_contrat - Permet l'affichage correct des heures pour les techniciens dans le PDF
This commit is contained in:
parent
0083976034
commit
34b3464132
8 changed files with 612 additions and 61 deletions
46
app/(app)/staff/avenants/nouveau/page.tsx
Normal file
46
app/(app)/staff/avenants/nouveau/page.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// app/(app)/staff/avenants/nouveau/page.tsx
|
||||
import { createSbServer } from "@/lib/supabaseServer";
|
||||
import NextDynamic from "next/dynamic";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const NouvelAvenantPageClient = NextDynamic<any>(
|
||||
() => import("@/components/staff/NouvelAvenantPageClient"),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
export default async function NouvelAvenantPage() {
|
||||
const sb = createSbServer();
|
||||
const { data: { user } } = await sb.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1 className="text-lg font-semibold">Accès refusé</h1>
|
||||
<p className="text-sm text-slate-600">Vous devez être connecté.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const { data: me } = await sb
|
||||
.from("staff_users")
|
||||
.select("is_staff")
|
||||
.eq("user_id", user.id)
|
||||
.maybeSingle();
|
||||
|
||||
const isStaff = !!me?.is_staff;
|
||||
if (!isStaff) {
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1 className="text-lg font-semibold">Accès refusé</h1>
|
||||
<p className="text-sm text-slate-600">Cette page est réservée au Staff.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="p-6">
|
||||
<NouvelAvenantPageClient />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// app/api/staff/amendments/generate-pdf/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createSbServer } from "@/lib/supabaseServer";
|
||||
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
// Fonction de formatage d'une date en "DD/MM/YYYY"
|
||||
|
|
@ -174,11 +174,11 @@ export async function POST(request: NextRequest) {
|
|||
// Déterminer les éléments avenantés
|
||||
const elementsAvenantes = amendmentData.elements || [];
|
||||
let elementsText = "";
|
||||
if (elementsAvenantes.includes("objet")) elementsText += "Objet, ";
|
||||
if (elementsAvenantes.includes("duree")) elementsText += "Durée de l'engagement, ";
|
||||
if (elementsAvenantes.includes("lieu_horaire")) elementsText += "Lieu et horaires, ";
|
||||
if (elementsAvenantes.includes("remuneration")) elementsText += "Rémunération, ";
|
||||
elementsText = elementsText.replace(/, $/, ""); // Retirer la virgule finale
|
||||
if (elementsAvenantes.includes("objet")) elementsText += "Objet,";
|
||||
if (elementsAvenantes.includes("duree")) elementsText += "Durée de l'engagement,";
|
||||
if (elementsAvenantes.includes("lieu_horaire")) elementsText += "Lieu et horaires,";
|
||||
if (elementsAvenantes.includes("remuneration")) elementsText += "Rémunération,";
|
||||
elementsText = elementsText.replace(/,$/, ""); // Retirer la virgule finale
|
||||
|
||||
// Préparer les données pour le PDF (valeurs du contrat ou de l'avenant)
|
||||
const professionData = amendmentData.objet_data || {};
|
||||
|
|
@ -187,8 +187,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
// Déterminer la catégorie professionnelle
|
||||
const profession = professionData.profession_label || contract.profession || "";
|
||||
const isTechnicien = contract.type_de_contrat !== "CDD d'usage";
|
||||
let employee_catpro = isTechnicien ? "Technicien" : "Artiste";
|
||||
let employee_catpro = contract.categorie_pro || "Artiste";
|
||||
if (profession === "Metteur en scène") {
|
||||
employee_catpro = "Metteur en scène";
|
||||
}
|
||||
|
|
@ -237,7 +236,7 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
// Panier repas
|
||||
const panierRepas = contract.panier_repas === "Oui" ? 1 : 0;
|
||||
const panierRepas = contract.panier_repas === "Oui" ? 1 : "";
|
||||
const panierRepasCCN = contract.panier_repas === "Oui" ? "Oui" : "Non";
|
||||
const montantPanierRepas = contract.montant_panier_repas || "20,20";
|
||||
|
||||
|
|
@ -295,7 +294,7 @@ export async function POST(request: NextRequest) {
|
|||
representations: cachetsRepresentations,
|
||||
repetitions: cachetsRepetitions,
|
||||
heures: heures,
|
||||
heuresparjour: 8 // Valeur par défaut
|
||||
heuresparjour: contract.nombre_d_heures_par_jour || 0
|
||||
},
|
||||
imageUrl: "data:image/png;base64" // Placeholder pour la signature
|
||||
};
|
||||
|
|
@ -356,23 +355,20 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
console.log("PDF uploadé sur S3:", s3Key);
|
||||
|
||||
// Générer un lien présigné
|
||||
// Générer un lien présigné pour GET (visualisation)
|
||||
const presignedUrl = await getSignedUrl(
|
||||
s3Client,
|
||||
new PutObjectCommand({
|
||||
new GetObjectCommand({
|
||||
Bucket: "odentas-docs",
|
||||
Key: s3Key,
|
||||
}),
|
||||
{ expiresIn: 3600 } // 1 heure
|
||||
);
|
||||
|
||||
// Transformer en GET URL
|
||||
const getPresignedUrl = presignedUrl.replace("?X-Amz-Algorithm", "?X-Amz-Algorithm");
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
s3Key,
|
||||
presignedUrl: getPresignedUrl,
|
||||
presignedUrl,
|
||||
filename
|
||||
});
|
||||
|
||||
|
|
|
|||
525
components/staff/NouvelAvenantPageClient.tsx
Normal file
525
components/staff/NouvelAvenantPageClient.tsx
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search, X, Loader2, CheckCircle2, Calendar, ArrowLeft, FileText } from "lucide-react";
|
||||
import {
|
||||
Amendment,
|
||||
AmendmentElementType,
|
||||
ContractSearchResult,
|
||||
OriginalContractData
|
||||
} from "@/types/amendments";
|
||||
import AmendmentObjetForm from "@/components/staff/amendments/AmendmentObjetForm";
|
||||
import AmendmentDureeForm from "@/components/staff/amendments/AmendmentDureeForm";
|
||||
import AmendmentRemunerationForm from "@/components/staff/amendments/AmendmentRemunerationForm";
|
||||
|
||||
export default function NouvelAvenantPageClient() {
|
||||
const router = useRouter();
|
||||
|
||||
// États
|
||||
const [step, setStep] = useState<"search" | "form">("search");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<ContractSearchResult[]>([]);
|
||||
const [selectedContract, setSelectedContract] = useState<OriginalContractData | null>(null);
|
||||
|
||||
// Données du formulaire
|
||||
const [dateEffet, setDateEffet] = useState("");
|
||||
const [dateSignature, setDateSignature] = useState("");
|
||||
const [selectedElements, setSelectedElements] = useState<AmendmentElementType[]>([]);
|
||||
|
||||
// Données spécifiques selon les éléments
|
||||
const [objetData, setObjetData] = useState<any>({});
|
||||
const [dureeData, setDureeData] = useState<any>({});
|
||||
const [remunerationData, setRemunerationData] = useState<any>({});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// PDF generation
|
||||
const [isGeneratingPdf, setIsGeneratingPdf] = useState(false);
|
||||
const [pdfPresignedUrl, setPdfPresignedUrl] = useState<string | null>(null);
|
||||
|
||||
// Recherche de contrats (debounced)
|
||||
useEffect(() => {
|
||||
if (searchTerm.length < 2) {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/staff/contracts/search?q=${encodeURIComponent(searchTerm)}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSearchResults(data.rows || data.contracts || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur recherche contrats:", error);
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
}, 400);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
|
||||
// Sélection d'un contrat
|
||||
const handleSelectContract = async (contractId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/staff/contracts/${contractId}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const contract = data.contract || data;
|
||||
|
||||
console.log("Données du contrat reçues:", contract);
|
||||
|
||||
// Mapper les données du contrat vers OriginalContractData
|
||||
const contractData: OriginalContractData = {
|
||||
id: contract.id,
|
||||
contract_number: contract.contract_number,
|
||||
employee_id: contract.employee_id,
|
||||
employee_name: contract.employee_name,
|
||||
employee_matricule: contract.employee_matricule,
|
||||
org_id: contract.org_id,
|
||||
organization_name: contract.structure,
|
||||
type_de_contrat: contract.type_de_contrat,
|
||||
categorie_pro: contract.categorie_pro,
|
||||
profession: contract.profession,
|
||||
production_name: contract.production_name,
|
||||
numero_objet: contract.n_objet,
|
||||
start_date: contract.start_date,
|
||||
end_date: contract.end_date,
|
||||
nb_representations: contract.cachets_representations || contract.nb_representations,
|
||||
nb_repetitions: contract.services_repetitions || contract.nb_services_repetition,
|
||||
nb_heures: contract.nombre_d_heures || contract.heures_total,
|
||||
dates_representations: contract.jours_representations || contract.dates_representations,
|
||||
dates_repetitions: contract.jours_repetitions || contract.dates_repetitions,
|
||||
jours_travail: contract.jours_travail_non_artiste || contract.jours_travail,
|
||||
lieu_travail: contract.lieu_travail,
|
||||
gross_pay: contract.gross_pay || contract.brut,
|
||||
precisions_salaire: contract.precisions_salaire,
|
||||
type_salaire: contract.type_salaire,
|
||||
};
|
||||
|
||||
setSelectedContract(contractData);
|
||||
|
||||
// Pré-remplir les données avec les valeurs du contrat
|
||||
setObjetData({
|
||||
profession_code: contractData.profession?.split(" - ")[0] || "",
|
||||
profession_label: contractData.profession?.split(" - ")[1] || contractData.profession || "",
|
||||
production_name: contractData.production_name || "",
|
||||
production_numero_objet: contractData.numero_objet || "",
|
||||
});
|
||||
|
||||
setDureeData({
|
||||
date_debut: contractData.start_date || "",
|
||||
date_fin: contractData.end_date || "",
|
||||
nb_representations: contractData.nb_representations || "",
|
||||
nb_repetitions: contractData.nb_repetitions || "",
|
||||
nb_heures: contractData.nb_heures || "",
|
||||
dates_representations: contractData.dates_representations || "",
|
||||
dates_repetitions: contractData.dates_repetitions || "",
|
||||
jours_travail: contractData.jours_travail || "",
|
||||
});
|
||||
|
||||
setRemunerationData({
|
||||
gross_pay: contractData.gross_pay || "",
|
||||
precisions_salaire: contractData.precisions_salaire || "",
|
||||
type_salaire: contractData.type_salaire || "Brut",
|
||||
});
|
||||
|
||||
setStep("form");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur chargement contrat:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle élément à avenanter
|
||||
const toggleElement = (element: AmendmentElementType) => {
|
||||
if (selectedElements.includes(element)) {
|
||||
setSelectedElements(selectedElements.filter((e) => e !== element));
|
||||
} else {
|
||||
setSelectedElements([...selectedElements, element]);
|
||||
}
|
||||
};
|
||||
|
||||
// Validation
|
||||
const canSubmit = useMemo(() => {
|
||||
if (!selectedContract || !dateEffet || selectedElements.length === 0) return false;
|
||||
return true;
|
||||
}, [selectedContract, dateEffet, selectedElements]);
|
||||
|
||||
// Génération du PDF
|
||||
const handleGeneratePdf = async () => {
|
||||
if (!canSubmit || !selectedContract) return;
|
||||
|
||||
setIsGeneratingPdf(true);
|
||||
try {
|
||||
const amendmentData = {
|
||||
contract_id: selectedContract.id,
|
||||
date_effet: dateEffet,
|
||||
date_signature: dateSignature || undefined,
|
||||
elements: selectedElements,
|
||||
objet_data: selectedElements.includes("objet") ? objetData : undefined,
|
||||
duree_data: selectedElements.includes("duree") ? dureeData : undefined,
|
||||
remuneration_data: selectedElements.includes("remuneration") ? remunerationData : undefined,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/staff/amendments/generate-pdf", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
contractId: selectedContract.id,
|
||||
amendmentData,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || "Erreur lors de la génération du PDF");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setPdfPresignedUrl(data.presignedUrl);
|
||||
} catch (error: any) {
|
||||
console.error("Erreur génération PDF:", error);
|
||||
alert("Erreur lors de la génération du PDF: " + error.message);
|
||||
} finally {
|
||||
setIsGeneratingPdf(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Soumission
|
||||
const handleSubmit = async () => {
|
||||
if (!canSubmit) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const amendment: Amendment = {
|
||||
contract_id: selectedContract!.id,
|
||||
contract_number: selectedContract!.contract_number,
|
||||
employee_name: selectedContract!.employee_name,
|
||||
organization_name: selectedContract!.organization_name,
|
||||
date_effet: dateEffet,
|
||||
date_signature: dateSignature || undefined,
|
||||
status: "draft",
|
||||
elements: selectedElements,
|
||||
objet_data: selectedElements.includes("objet") ? objetData : undefined,
|
||||
duree_data: selectedElements.includes("duree") ? dureeData : undefined,
|
||||
remuneration_data: selectedElements.includes("remuneration") ? remunerationData : undefined,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// TODO: Envoyer à l'API
|
||||
// const response = await fetch('/api/staff/amendments', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify(amendment),
|
||||
// });
|
||||
|
||||
router.push("/staff/avenants");
|
||||
} catch (error) {
|
||||
console.error("Erreur création avenant:", error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-";
|
||||
const [y, m, d] = dateStr.split("-");
|
||||
return `${d}/${m}/${y}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{/* Header avec bouton retour */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => router.push("/staff/avenants")}
|
||||
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5 text-slate-600" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-2xl font-bold text-slate-900">
|
||||
{step === "search" ? "Rechercher un contrat" : "Nouvel avenant"}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
{step === "search"
|
||||
? "Sélectionnez le contrat à avenanter"
|
||||
: "Définissez les modifications à apporter au contrat"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Étape 1: Recherche du contrat */}
|
||||
{step === "search" && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6 space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher par n° contrat, salarié, organisation, référence..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-11 pr-4 py-3 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isSearching && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-indigo-600" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && searchResults.length > 0 && (
|
||||
<div className="space-y-2 max-h-[500px] overflow-y-auto">
|
||||
{searchResults.map((contract) => (
|
||||
<button
|
||||
key={contract.id}
|
||||
onClick={() => handleSelectContract(contract.id)}
|
||||
className="w-full p-4 border rounded-lg hover:bg-slate-50 hover:border-indigo-300 transition-all text-left"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-slate-900">
|
||||
{contract.contract_number}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">
|
||||
{contract.employee_name}
|
||||
{contract.employee_matricule && ` (${contract.employee_matricule})`}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
{contract.organization_name || contract.structure}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs text-slate-500">
|
||||
{formatDate(contract.start_date)} - {formatDate(contract.end_date)}
|
||||
</div>
|
||||
{contract.profession && (
|
||||
<div className="text-xs text-slate-500 mt-1">{contract.profession}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSearching && searchTerm.length >= 2 && searchResults.length === 0 && (
|
||||
<div className="py-12 text-center">
|
||||
<FileText className="h-12 w-12 text-slate-300 mx-auto mb-3" />
|
||||
<p className="text-slate-600">Aucun contrat trouvé pour "{searchTerm}"</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searchTerm.length < 2 && (
|
||||
<div className="py-12 text-center">
|
||||
<Search className="h-12 w-12 text-slate-300 mx-auto mb-3" />
|
||||
<p className="text-slate-500 text-sm">
|
||||
Tapez au moins 2 caractères pour rechercher
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Étape 2: Formulaire de l'avenant */}
|
||||
{step === "form" && selectedContract && (
|
||||
<div className="space-y-6">
|
||||
{/* Infos du contrat */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-slate-900 text-lg">
|
||||
Contrat {selectedContract.contract_number}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600 mt-1">
|
||||
{selectedContract.employee_name} • {selectedContract.organization_name}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
{formatDate(selectedContract.start_date)} - {formatDate(selectedContract.end_date)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setStep("search")}
|
||||
className="text-slate-400 hover:text-slate-600 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dates */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<h2 className="font-semibold text-slate-900 mb-4">Dates de l'avenant</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Date d'effet de l'avenant <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
||||
<input
|
||||
type="date"
|
||||
value={dateEffet}
|
||||
onChange={(e) => setDateEffet(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>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Date de signature
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" />
|
||||
<input
|
||||
type="date"
|
||||
value={dateSignature}
|
||||
onChange={(e) => setDateSignature(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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Éléments à avenanter */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<h2 className="font-semibold text-slate-900 mb-4">
|
||||
Éléments à avenanter <span className="text-red-500">*</span>
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ value: "objet" as const, label: "Objet (profession, production)" },
|
||||
{ value: "duree" as const, label: "Durée de l'engagement" },
|
||||
{ value: "lieu_horaire" as const, label: "Lieu et horaires" },
|
||||
{ value: "remuneration" as const, label: "Rémunération" },
|
||||
].map((element) => (
|
||||
<button
|
||||
key={element.value}
|
||||
onClick={() => toggleElement(element.value)}
|
||||
className={`p-4 border rounded-lg text-left transition-all ${
|
||||
selectedElements.includes(element.value)
|
||||
? "bg-indigo-50 border-indigo-500 text-indigo-700"
|
||||
: "bg-white border-slate-200 text-slate-700 hover:bg-slate-50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-5 h-5 rounded border flex items-center justify-center ${
|
||||
selectedElements.includes(element.value)
|
||||
? "bg-indigo-600 border-indigo-600"
|
||||
: "border-slate-300"
|
||||
}`}
|
||||
>
|
||||
{selectedElements.includes(element.value) && (
|
||||
<CheckCircle2 className="h-3 w-3 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{element.label}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulaires conditionnels */}
|
||||
{selectedElements.includes("objet") && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<AmendmentObjetForm
|
||||
originalData={selectedContract}
|
||||
data={objetData}
|
||||
onChange={setObjetData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedElements.includes("duree") && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<AmendmentDureeForm
|
||||
originalData={selectedContract}
|
||||
data={dureeData}
|
||||
onChange={setDureeData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedElements.includes("remuneration") && (
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<AmendmentRemunerationForm
|
||||
originalData={selectedContract}
|
||||
data={remunerationData}
|
||||
onChange={setRemunerationData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lien PDF si généré */}
|
||||
{pdfPresignedUrl && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-green-900">PDF généré avec succès</div>
|
||||
<div className="text-sm text-green-700">Le PDF de l'avenant est prêt</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={pdfPresignedUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
|
||||
>
|
||||
Voir le PDF
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="bg-white rounded-xl border shadow-sm p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
onClick={handleGeneratePdf}
|
||||
disabled={!canSubmit || isGeneratingPdf}
|
||||
className="px-6 py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 font-medium"
|
||||
>
|
||||
{isGeneratingPdf && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
Créer le PDF
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/staff/avenants")}
|
||||
className="px-6 py-3 text-slate-700 hover:bg-slate-100 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit || isSubmitting}
|
||||
className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 font-medium"
|
||||
>
|
||||
{isSubmitting && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
Créer l'avenant
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FileText, Plus, Search } from "lucide-react";
|
||||
import { Amendment } from "@/types/amendments";
|
||||
import NouvelAvenantModal from "./amendments/NouvelAvenantModal";
|
||||
|
||||
interface StaffAvenantsPageClientProps {
|
||||
initialData: Amendment[];
|
||||
}
|
||||
|
||||
export default function StaffAvenantsPageClient({ initialData }: StaffAvenantsPageClientProps) {
|
||||
const router = useRouter();
|
||||
const [amendments, setAmendments] = useState<Amendment[]>(initialData);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const filteredAmendments = amendments.filter((amendment) => {
|
||||
|
|
@ -70,7 +70,7 @@ export default function StaffAvenantsPageClient({ initialData }: StaffAvenantsPa
|
|||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
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" />
|
||||
|
|
@ -106,7 +106,7 @@ export default function StaffAvenantsPageClient({ initialData }: StaffAvenantsPa
|
|||
</p>
|
||||
{!searchTerm && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
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" />
|
||||
|
|
@ -174,16 +174,6 @@ export default function StaffAvenantsPageClient({ initialData }: StaffAvenantsPa
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal */}
|
||||
<NouvelAvenantModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onAmendmentCreated={(amendment) => {
|
||||
setAmendments([amendment, ...amendments]);
|
||||
setIsModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,18 +10,16 @@ interface AmendmentDureeFormProps {
|
|||
originalData: OriginalContractData;
|
||||
data: any;
|
||||
onChange: (data: any) => void;
|
||||
onSubModalChange?: (isOpen: boolean) => void; // Nouveau prop pour notifier le parent
|
||||
}
|
||||
|
||||
export default function AmendmentDureeForm({
|
||||
originalData,
|
||||
data,
|
||||
onChange,
|
||||
onSubModalChange,
|
||||
}: AmendmentDureeFormProps) {
|
||||
// Déterminer si c'est un contrat artiste (CDDU) ou technicien
|
||||
const isArtist = originalData.type_de_contrat === "CDD d'usage";
|
||||
const isTechnician = !isArtist;
|
||||
// Déterminer si c'est un contrat artiste ou technicien
|
||||
const isTechnician = originalData.categorie_pro === "Technicien";
|
||||
const isArtist = !isTechnician;
|
||||
|
||||
// États pour les calendriers
|
||||
const [showDateDebut, setShowDateDebut] = useState(false);
|
||||
|
|
@ -37,12 +35,6 @@ export default function AmendmentDureeForm({
|
|||
>("representations");
|
||||
const [pendingDates, setPendingDates] = useState<string[]>([]);
|
||||
|
||||
// Notifier le parent quand un sous-modal est ouvert/fermé
|
||||
useEffect(() => {
|
||||
const isAnyModalOpen = showDateDebut || showDateFin || showDatesRep || showDatesServ || showJoursTravail || quantityModalOpen;
|
||||
onSubModalChange?.(isAnyModalOpen);
|
||||
}, [showDateDebut, showDateFin, showDatesRep, showDatesServ, showJoursTravail, quantityModalOpen, onSubModalChange]);
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "-";
|
||||
const [y, m, d] = dateStr.split("-");
|
||||
|
|
@ -89,9 +81,13 @@ export default function AmendmentDureeForm({
|
|||
hasMultiMonth: boolean;
|
||||
pdfFormatted: string;
|
||||
}) => {
|
||||
setPendingDates(result.selectedDates);
|
||||
setQuantityModalType("jours_travail");
|
||||
setQuantityModalOpen(true);
|
||||
// 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
|
||||
|
|
@ -117,12 +113,6 @@ export default function AmendmentDureeForm({
|
|||
nb_repetitions: nbDates,
|
||||
});
|
||||
break;
|
||||
case "jours_travail":
|
||||
onChange({
|
||||
...data,
|
||||
jours_travail: result.pdfFormatted,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
setQuantityModalOpen(false);
|
||||
|
|
@ -287,7 +277,7 @@ export default function AmendmentDureeForm({
|
|||
<>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-700 mb-2">
|
||||
Nombre d'heures
|
||||
Nombre d'heures (global)
|
||||
</label>
|
||||
<div className="text-xs text-slate-500 mb-2">
|
||||
Actuellement : {originalData.nb_heures || "-"}
|
||||
|
|
@ -304,7 +294,11 @@ export default function AmendmentDureeForm({
|
|||
})
|
||||
}
|
||||
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>
|
||||
|
|
@ -335,6 +329,9 @@ export default function AmendmentDureeForm({
|
|||
{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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,12 @@ interface AmendmentObjetFormProps {
|
|||
originalData: OriginalContractData;
|
||||
data: any;
|
||||
onChange: (data: any) => void;
|
||||
onSubModalChange?: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export default function AmendmentObjetForm({
|
||||
originalData,
|
||||
data,
|
||||
onChange,
|
||||
onSubModalChange,
|
||||
}: AmendmentObjetFormProps) {
|
||||
const [professionsArtiste, setProfessionsArtiste] = useState<any[]>([]);
|
||||
const [professionsTechnicien, setProfessionsTechnicien] = useState<any[]>([]);
|
||||
|
|
@ -26,12 +24,6 @@ export default function AmendmentObjetForm({
|
|||
const [showProfessionList, setShowProfessionList] = useState(false);
|
||||
const [showProductionList, setShowProductionList] = useState(false);
|
||||
|
||||
// Notifier le parent quand les listes sont ouvertes/fermées
|
||||
useEffect(() => {
|
||||
const isAnyListOpen = showProfessionList || showProductionList;
|
||||
onSubModalChange?.(isAnyListOpen);
|
||||
}, [showProfessionList, showProductionList, onSubModalChange]);
|
||||
|
||||
// Déterminer le type de contrat (Artiste ou Technicien)
|
||||
const isArtist = originalData.type_de_contrat === "CDD d'usage";
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ export default function NouvelAvenantModal({
|
|||
const [hasOpenSubModal, setHasOpenSubModal] = useState(false);
|
||||
const [isSubModalOpen, setIsSubModalOpen] = useState(false);
|
||||
|
||||
// Synchroniser hasOpenSubModal avec isSubModalOpen
|
||||
useEffect(() => {
|
||||
setHasOpenSubModal(isSubModalOpen);
|
||||
}, [isSubModalOpen]);
|
||||
|
||||
// Reset au changement d'état du modal
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
|
|
@ -118,6 +123,7 @@ export default function NouvelAvenantModal({
|
|||
org_id: contract.org_id,
|
||||
organization_name: contract.structure,
|
||||
type_de_contrat: contract.type_de_contrat,
|
||||
categorie_pro: contract.categorie_pro,
|
||||
profession: contract.profession,
|
||||
production_name: contract.production_name,
|
||||
numero_objet: contract.n_objet,
|
||||
|
|
@ -464,7 +470,6 @@ export default function NouvelAvenantModal({
|
|||
originalData={selectedContract}
|
||||
data={objetData}
|
||||
onChange={setObjetData}
|
||||
onSubModalChange={setIsSubModalOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -473,7 +478,6 @@ export default function NouvelAvenantModal({
|
|||
originalData={selectedContract}
|
||||
data={dureeData}
|
||||
onChange={setDureeData}
|
||||
onSubModalChange={setIsSubModalOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export interface OriginalContractData {
|
|||
org_id: string;
|
||||
organization_name?: string;
|
||||
type_de_contrat: string;
|
||||
categorie_pro?: string;
|
||||
|
||||
// Objet
|
||||
profession?: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue