feat: Ajout modal création en masse de paies avec tous les champs corrects

This commit is contained in:
odentas 2025-10-28 14:48:04 +01:00
parent eb3133866e
commit 2aeac651c1
3 changed files with 748 additions and 2 deletions

View file

@ -0,0 +1,99 @@
import { NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
async function assertStaff(sb: ReturnType<typeof createSbServer>, userId: string) {
const { data: me } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", userId)
.maybeSingle();
return !!me?.is_staff;
}
export async function POST(req: Request) {
const supabase = createSbServer();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "Non authentifié." }, { status: 401 });
const isStaff = await assertStaff(supabase, user.id);
if (!isStaff) return NextResponse.json({ error: "Accès réservé au staff." }, { status: 403 });
const body = await req.json().catch(() => ({}));
const { payslips } = body;
if (!payslips || !Array.isArray(payslips) || payslips.length === 0) {
return NextResponse.json({ error: "Aucune paie fournie" }, { status: 400 });
}
console.log(`📊 Création en masse de ${payslips.length} paies...`);
const results = [];
const errors = [];
for (const payslipData of payslips) {
try {
if (!payslipData.contract_id) {
errors.push({
contract_id: payslipData.contract_id,
error: "ID du contrat manquant"
});
continue;
}
// Récupérer les informations du contrat pour obtenir l'organization_id
const { data: contract, error: contractError } = await supabase
.from("cddu_contracts")
.select("org_id")
.eq("id", payslipData.contract_id)
.single();
if (contractError || !contract) {
console.error(`❌ Erreur récupération contrat ${payslipData.contract_id}:`, contractError);
errors.push({
contract_id: payslipData.contract_id,
error: "Contrat non trouvé"
});
continue;
}
// Ajouter l'organization_id aux données
const fullPayslipData = {
...payslipData,
organization_id: contract.org_id
};
// Créer la paie
const { data: payslip, error: insertError } = await supabase
.from("payslips")
.insert([fullPayslipData])
.select("*")
.single();
if (insertError) {
console.error(`❌ Erreur création payslip pour contrat ${payslipData.contract_id}:`, insertError);
errors.push({
contract_id: payslipData.contract_id,
error: insertError.message
});
continue;
}
console.log(`✅ Paie créée pour contrat ${payslipData.contract_id}`);
results.push(payslip);
} catch (error) {
console.error(`❌ Erreur lors de la création de la paie:`, error);
errors.push({
contract_id: payslipData.contract_id,
error: error instanceof Error ? error.message : 'Erreur inconnue'
});
}
}
console.log(`📊 Résultat: ${results.length} paies créées, ${errors.length} erreurs`);
return NextResponse.json({
created: results.length,
errors: errors.length > 0 ? errors : undefined,
payslips: results
});
}

View file

@ -13,6 +13,7 @@ import BulkESignConfirmModal from "./BulkESignConfirmModal";
import { BulkEmployeeReminderModal } from "./contracts/BulkEmployeeReminderModal";
import { EmployeeReminderModal } from "./contracts/EmployeeReminderModal";
import { SmartReminderModal, type SmartReminderContract, type ReminderAction } from "./contracts/SmartReminderModal";
import BulkPayslipModal from "./contracts/BulkPayslipModal";
// Utility function to format dates as DD/MM/YYYY
function formatDate(dateString: string | null | undefined): string {
@ -176,6 +177,7 @@ type Contract = {
last_employer_notification_at?: string | null;
last_employee_notification_at?: string | null;
production_name?: string | null;
analytique?: string | null;
salaries?: {
salarie?: string | null;
nom?: string | null;
@ -290,10 +292,12 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
const [showDpaeModal, setShowDpaeModal] = useState(false);
const [showEtatContratModal, setShowEtatContratModal] = useState(false);
const [showEtatPaieModal, setShowEtatPaieModal] = useState(false);
const [showAnalytiqueModal, setShowAnalytiqueModal] = useState(false);
const [showSalaryModal, setShowSalaryModal] = useState(false);
const [showActionMenu, setShowActionMenu] = useState(false);
const [showESignMenu, setShowESignMenu] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showBulkPayslipModal, setShowBulkPayslipModal] = useState(false);
// Quick filter counts
const [countDpaeAFaire, setCountDpaeAFaire] = useState<number | null>(null);
@ -1167,6 +1171,36 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
setIsESignCancelled(false);
};
// Fonction pour créer les paies en masse
const handleBulkPayslipSubmit = async (payslips: any[]) => {
try {
const response = await fetch('/api/staff/payslips/bulk-create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ payslips }),
});
if (!response.ok) {
throw new Error('Erreur lors de la création des paies');
}
const result = await response.json();
toast.success(`${result.created || payslips.length} paie(s) créée(s) avec succès`);
// Rafraîchir les données
handleRefresh();
// Désélectionner les contrats
setSelectedContractIds(new Set());
} catch (error) {
console.error('Erreur création paies:', error);
throw error;
}
};
// Fonction pour extraire les notifications directement des données de contrats
// Plus besoin d'appel API séparé, les timestamps sont maintenant dans cddu_contracts
const updateNotificationsFromRows = (contracts: Contract[]) => {
@ -1190,7 +1224,7 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
// Debounce searches when filters change
useEffect(() => {
// if no filters applied, prefer initial data
const noFilters = !q && !structureFilter && !typeFilter && etatContratFilters.size === 0 && !etatPaieFilter && !dpaeFilter && !signatureFilter && !startFrom && !startTo && sortField === 'start_date' && sortOrder === 'desc';
const noFilters = !q && !structureFilter && !typeFilter && etatContratFilters.size === 0 && !etatPaieFilter && !dpaeFilter && !signatureFilter && !startFrom && !startTo && !endFrom && !endTo && sortField === 'start_date' && sortOrder === 'desc';
if (noFilters) {
setRows(initialData || []);
return;
@ -1199,7 +1233,7 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
const t = setTimeout(() => fetchServer(0), 300);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [q, structureFilter, typeFilter, etatContratFilters, etatPaieFilter, dpaeFilter, signatureFilter, startFrom, startTo, sortField, sortOrder, limit]);
}, [q, structureFilter, typeFilter, etatContratFilters, etatPaieFilter, dpaeFilter, signatureFilter, startFrom, startTo, endFrom, endTo, sortField, sortOrder, limit]);
// Récupérer les notifications quand les données changent
useEffect(() => {
@ -1865,6 +1899,17 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
/>
<div className="absolute right-0 mt-1 w-48 bg-white rounded-md shadow-lg z-20 border border-gray-200">
<div className="py-1">
<button
onClick={() => {
setShowBulkPayslipModal(true);
setShowActionMenu(false);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors flex items-center gap-2"
>
<Euro className="w-4 h-4" />
Ajout de paie
</button>
<div className="border-t border-gray-200 my-1"></div>
<button
onClick={() => {
setShowDpaeModal(true);
@ -1895,6 +1940,16 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
<BarChart3 className="w-4 h-4" />
Modifier État Paie
</button>
<button
onClick={() => {
setShowAnalytiqueModal(true);
setShowActionMenu(false);
}}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition-colors flex items-center gap-2"
>
<FileText className="w-4 h-4" />
Modifier Analytique
</button>
<button
onClick={() => {
viewSelectedDetails();
@ -2250,6 +2305,30 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
</div>
)}
{/* Modal Action groupée Analytique */}
{showAnalytiqueModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md">
<h3 className="text-lg font-semibold mb-4">Action groupée - Analytique</h3>
<p className="text-sm text-gray-600 mb-4">
Modifier l'analytique pour {selectedContractIds.size} contrat{selectedContractIds.size > 1 ? 's' : ''}
</p>
<AnalytiqueActionModal
selectedContracts={selectedContracts}
onClose={() => setShowAnalytiqueModal(false)}
onSuccess={(updatedContracts) => {
// Mettre à jour les contrats dans la liste
setRows(prev => prev.map(row => {
const updated = updatedContracts.find(u => u.id === row.id);
return updated ? { ...row, analytique: updated.analytique, production_name: updated.analytique } : row;
}));
setShowAnalytiqueModal(false);
}}
/>
</div>
</div>
)}
{/* Modal Saisir brut */}
{showSalaryModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
@ -2332,6 +2411,14 @@ function ContractsGridImpl({ initialData, activeOrgId }: { initialData: Contract
progressContracts={smartReminderProgress.length > 0 ? smartReminderProgress : undefined}
/>
{/* Modal d'ajout de paie en masse */}
<BulkPayslipModal
isOpen={showBulkPayslipModal}
onClose={() => setShowBulkPayslipModal(false)}
contracts={selectedContracts}
onSubmit={handleBulkPayslipSubmit}
/>
{/* Modal de progression pour l'envoi des e-signatures */}
<BulkESignProgressModal
isOpen={showESignProgressModal}
@ -2654,6 +2741,107 @@ function EtatPaieActionModal({
);
}
// Modal pour l'analytique
function AnalytiqueActionModal({
selectedContracts,
onClose,
onSuccess
}: {
selectedContracts: Contract[];
onClose: () => void;
onSuccess: (contracts: { id: string; analytique: string }[]) => void;
}) {
const [newAnalytique, setNewAnalytique] = useState<string>('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
if (!newAnalytique.trim()) {
toast.error("Veuillez saisir un code analytique");
return;
}
setLoading(true);
try {
const response = await fetch('/api/staff/contracts/bulk-update-analytique', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contractIds: selectedContracts.map(c => c.id),
analytique: newAnalytique.trim()
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Erreur lors de la mise à jour');
}
const result = await response.json();
toast.success(`${result.count} contrat(s) mis à jour`);
onSuccess(result.contracts);
} catch (error: any) {
console.error('Erreur:', error);
toast.error(error.message || 'Erreur lors de la mise à jour des contrats');
} finally {
setLoading(false);
}
};
return (
<>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nouveau code analytique
</label>
<input
type="text"
value={newAnalytique}
onChange={(e) => setNewAnalytique(e.target.value)}
placeholder="Ex: Production 2025"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
/>
<p className="text-xs text-gray-500 mt-1">
Ce code sera appliqué au champ "Analytique" de tous les contrats sélectionnés
</p>
</div>
<div className="border rounded-lg p-3 max-h-40 overflow-y-auto">
<p className="text-sm font-medium text-gray-700 mb-2">Contrats sélectionnés :</p>
{selectedContracts.map(contract => (
<div key={contract.id} className="text-sm text-gray-600 py-1">
{contract.contract_number || contract.id} - {formatEmployeeName(contract)}
{contract.analytique && (
<span className="text-xs text-gray-400 ml-2">
(actuel: {contract.analytique})
</span>
)}
</div>
))}
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 border border-gray-300 rounded hover:bg-gray-50"
disabled={loading}
>
Annuler
</button>
<button
onClick={handleSubmit}
disabled={!newAnalytique.trim() || loading}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Mise à jour...' : 'Appliquer'}
</button>
</div>
</>
);
}
// Modal pour saisir le brut
function SalaryInputModal({
selectedContracts,

View file

@ -0,0 +1,459 @@
// components/staff/contracts/BulkPayslipModal.tsx
"use client";
import { useState, useMemo, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { X, Plus, Check, AlertCircle } from "lucide-react";
import { toast } from "sonner";
type Contract = {
id: string;
contract_number?: string | null;
employee_name?: string | null;
production_name?: string | null;
start_date?: string | null;
end_date?: string | null;
salaries?: {
salarie?: string | null;
nom?: string | null;
prenom?: string | null;
} | null;
};
type PayslipData = {
contract_id: string;
pay_number: string;
pay_date: string;
period_start: string;
period_end: string;
gross_amount: string;
net_amount: string;
net_after_withholding: string;
employer_cost: string;
aem_status: string;
storage_path: string;
storage_url: string;
processed: boolean;
transfer_done: boolean;
};
type BulkPayslipModalProps = {
isOpen: boolean;
onClose: () => void;
contracts: Contract[];
onSubmit: (payslips: PayslipData[]) => Promise<void>;
};
// Fonction utilitaire pour formater le nom du salarié
function formatEmployeeName(contract: Contract): string {
if (contract.salaries?.salarie) {
return contract.salaries.salarie;
}
if (contract.salaries?.nom || contract.salaries?.prenom) {
const nom = (contract.salaries.nom || '').toUpperCase().trim();
const prenom = (contract.salaries.prenom || '').trim();
return [nom, prenom].filter(Boolean).join(' ');
}
return contract.employee_name || "—";
}
export default function BulkPayslipModal({ isOpen, onClose, contracts, onSubmit }: BulkPayslipModalProps) {
// État pour les valeurs communes (pré-remplissage)
const [commonValues, setCommonValues] = useState({
pay_date: "",
period_start: "",
period_end: "",
aem_status: "À traiter",
processed: false,
transfer_done: false,
});
// État pour les données de chaque contrat
const [payslipsData, setPayslipsData] = useState<Record<string, Partial<PayslipData>>>({});
// Initialiser les données avec les dates de contrat (une seule fois)
useEffect(() => {
const initialData: Record<string, Partial<PayslipData>> = {};
contracts.forEach((contract) => {
initialData[contract.id] = {
contract_id: contract.id,
pay_number: "",
pay_date: "",
period_start: contract.start_date?.slice(0, 10) || "",
period_end: contract.end_date?.slice(0, 10) || "",
gross_amount: "",
net_amount: "",
net_after_withholding: "",
employer_cost: "",
aem_status: "À traiter",
storage_path: "",
storage_url: "",
processed: false,
transfer_done: false,
};
});
setPayslipsData(initialData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Seulement au montage du composant
// Mettre à jour une valeur pour un contrat spécifique
const updatePayslipData = (contractId: string, field: keyof PayslipData, value: any) => {
setPayslipsData((prev) => ({
...prev,
[contractId]: {
...prev[contractId],
[field]: value,
},
}));
};
// Appliquer les valeurs communes à tous les contrats
const applyCommonValues = () => {
setPayslipsData((prev) => {
const updated = { ...prev };
Object.keys(updated).forEach((contractId) => {
updated[contractId] = {
...updated[contractId],
pay_date: commonValues.pay_date || updated[contractId]?.pay_date,
aem_status: commonValues.aem_status || updated[contractId]?.aem_status,
processed: commonValues.processed,
transfer_done: commonValues.transfer_done,
};
});
return updated;
});
toast.success("Valeurs communes appliquées à tous les contrats");
};
// Validation et soumission
const handleSubmit = async () => {
const payslips: PayslipData[] = [];
const errors: string[] = [];
contracts.forEach((contract) => {
const data = payslipsData[contract.id];
if (!data) return;
// Validation basique
if (!data.pay_number?.trim()) {
errors.push(`Contrat ${contract.contract_number || contract.id}: n° de paie manquant`);
}
if (!data.pay_date) {
errors.push(`Contrat ${contract.contract_number || contract.id}: date de paie manquante`);
}
payslips.push({
contract_id: contract.id,
pay_number: data.pay_number || "",
pay_date: data.pay_date || "",
period_start: data.period_start || "",
period_end: data.period_end || "",
gross_amount: data.gross_amount || "",
net_amount: data.net_amount || "",
net_after_withholding: data.net_after_withholding || "",
employer_cost: data.employer_cost || "",
aem_status: data.aem_status || "À traiter",
storage_path: data.storage_path || "",
storage_url: data.storage_url || "",
processed: data.processed || false,
transfer_done: data.transfer_done || false,
});
});
if (errors.length > 0) {
toast.error(`Erreurs de validation: ${errors.join(", ")}`);
return;
}
try {
await onSubmit(payslips);
toast.success(`${payslips.length} paie(s) créée(s) avec succès`);
onClose();
} catch (error) {
console.error("Erreur lors de la création des paies:", error);
toast.error("Erreur lors de la création des paies");
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-2xl shadow-2xl w-[95vw] h-[90vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200">
<div>
<h2 className="text-2xl font-bold text-slate-900">Ajout de paies en masse</h2>
<p className="text-sm text-slate-600 mt-1">
{contracts.length} contrat{contracts.length > 1 ? "s" : ""} sélectionné{contracts.length > 1 ? "s" : ""}
</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-slate-100 rounded-full transition-colors"
>
<X className="w-6 h-6 text-slate-600" />
</button>
</div>
{/* Zone de valeurs communes */}
<div className="px-6 py-4 bg-gradient-to-r from-indigo-50 to-blue-50 border-b border-indigo-200">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 bg-indigo-600 rounded-full"></div>
<h3 className="text-sm font-semibold text-indigo-900">Valeurs communes (optionnel)</h3>
</div>
<div className="grid grid-cols-5 gap-3">
<div>
<Label className="text-xs text-slate-700">Date de paie</Label>
<Input
type="date"
value={commonValues.pay_date}
onChange={(e) => setCommonValues((prev) => ({ ...prev, pay_date: e.target.value }))}
className="h-9 bg-white border-indigo-200 focus:border-indigo-400"
/>
</div>
<div>
<Label className="text-xs text-slate-700">Statut AEM</Label>
<select
value={commonValues.aem_status}
onChange={(e) => setCommonValues((prev) => ({ ...prev, aem_status: e.target.value }))}
className="h-9 w-full bg-white border border-indigo-200 rounded-md focus:border-indigo-400 focus:ring-2 focus:ring-indigo-400 text-sm"
>
<option value="À traiter">À traiter</option>
<option value="OK">OK</option>
</select>
</div>
<div className="flex items-end gap-2">
<label className="flex items-center gap-2 text-xs text-slate-700 cursor-pointer">
<input
type="checkbox"
checked={commonValues.processed}
onChange={(e) => setCommonValues((prev) => ({ ...prev, processed: e.target.checked }))}
className="rounded border-indigo-300 text-indigo-600 focus:ring-indigo-500"
/>
Traité
</label>
<label className="flex items-center gap-2 text-xs text-slate-700 cursor-pointer">
<input
type="checkbox"
checked={commonValues.transfer_done}
onChange={(e) => setCommonValues((prev) => ({ ...prev, transfer_done: e.target.checked }))}
className="rounded border-indigo-300 text-indigo-600 focus:ring-indigo-500"
/>
Virement ok
</label>
</div>
<div className="flex items-end">
<Button
onClick={applyCommonValues}
className="w-full h-9 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
>
<Check className="w-4 h-4 mr-1" />
Appliquer
</Button>
</div>
</div>
</div>
{/* Liste des contrats avec leurs champs */}
<div className="flex-1 overflow-y-auto px-6 py-4">
<div className="space-y-4">
{contracts.map((contract, index) => {
const data = payslipsData[contract.id] || {};
return (
<div
key={contract.id}
className="bg-gradient-to-r from-slate-50 to-slate-100 rounded-xl p-4 border border-slate-200 hover:shadow-md transition-shadow"
>
{/* En-tête du contrat */}
<div className="flex items-center gap-3 mb-4 pb-3 border-b border-slate-300">
<div className="w-8 h-8 bg-indigo-600 text-white rounded-full flex items-center justify-center font-bold text-sm">
{index + 1}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-slate-900">
{formatEmployeeName(contract)}
</span>
<span className="text-xs text-slate-500"></span>
<span className="text-sm text-slate-600">
{contract.contract_number || "N/A"}
</span>
</div>
<div className="text-xs text-slate-500 mt-1">
{contract.production_name || "—"} {contract.start_date?.slice(0, 10)} {contract.end_date?.slice(0, 10)}
</div>
</div>
</div>
{/* Champs de la paie */}
<div className="grid grid-cols-4 gap-3">
{/* Ligne 1 */}
<div>
<Label className="text-xs text-slate-700 flex items-center gap-1">
N° de paie
<span className="text-red-500">*</span>
</Label>
<Input
value={data.pay_number || ""}
onChange={(e) => updatePayslipData(contract.id, "pay_number", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
placeholder="ex: 001"
/>
</div>
<div>
<Label className="text-xs text-slate-700 flex items-center gap-1">
Date de paie
<span className="text-red-500">*</span>
</Label>
<Input
type="date"
value={data.pay_date || ""}
onChange={(e) => updatePayslipData(contract.id, "pay_date", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
/>
</div>
<div>
<Label className="text-xs text-slate-700">Période début</Label>
<Input
type="date"
value={data.period_start || ""}
onChange={(e) => updatePayslipData(contract.id, "period_start", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
/>
</div>
<div>
<Label className="text-xs text-slate-700">Période fin</Label>
<Input
type="date"
value={data.period_end || ""}
onChange={(e) => updatePayslipData(contract.id, "period_end", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
/>
</div>
{/* Ligne 2 */}
<div>
<Label className="text-xs text-slate-700">Montant brut</Label>
<Input
type="number"
step="0.01"
value={data.gross_amount || ""}
onChange={(e) => updatePayslipData(contract.id, "gross_amount", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
placeholder="0.00"
/>
</div>
<div>
<Label className="text-xs text-slate-700">Net avant PAS</Label>
<Input
type="number"
step="0.01"
value={data.net_amount || ""}
onChange={(e) => updatePayslipData(contract.id, "net_amount", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
placeholder="0.00"
/>
</div>
<div>
<Label className="text-xs text-slate-700">Net à payer</Label>
<Input
type="number"
step="0.01"
value={data.net_after_withholding || ""}
onChange={(e) => updatePayslipData(contract.id, "net_after_withholding", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
placeholder="0.00"
/>
</div>
<div>
<Label className="text-xs text-slate-700">Coût total employeur</Label>
<Input
type="number"
step="0.01"
value={data.employer_cost || ""}
onChange={(e) => updatePayslipData(contract.id, "employer_cost", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
placeholder="0.00"
/>
</div>
{/* Ligne 3 */}
<div>
<Label className="text-xs text-slate-700">Statut AEM</Label>
<select
value={data.aem_status || "À traiter"}
onChange={(e) => updatePayslipData(contract.id, "aem_status", e.target.value)}
className="h-9 w-full bg-white border border-slate-300 rounded-md focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 text-sm"
>
<option value="À traiter">À traiter</option>
<option value="OK">OK</option>
</select>
</div>
<div className="col-span-3">
<Label className="text-xs text-slate-700">Chemin de stockage (S3)</Label>
<Input
value={data.storage_path || ""}
onChange={(e) => updatePayslipData(contract.id, "storage_path", e.target.value)}
className="h-9 bg-white border-slate-300 focus:border-indigo-500"
placeholder="ex: payslips/2025/..."
/>
</div>
{/* Ligne 4 - Checkboxes */}
<div className="col-span-4 flex items-center gap-4 pt-2">
<label className="flex items-center gap-2 text-xs text-slate-700 cursor-pointer">
<input
type="checkbox"
checked={data.processed || false}
onChange={(e) => updatePayslipData(contract.id, "processed", e.target.checked)}
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
/>
Traité
</label>
<label className="flex items-center gap-2 text-xs text-slate-700 cursor-pointer">
<input
type="checkbox"
checked={data.transfer_done || false}
onChange={(e) => updatePayslipData(contract.id, "transfer_done", e.target.checked)}
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
/>
Virement ok
</label>
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-slate-200 bg-slate-50 flex items-center justify-between rounded-b-2xl">
<div className="flex items-center gap-2 text-sm text-slate-600">
<AlertCircle className="w-4 h-4" />
<span>Les champs marqués d'un <span className="text-red-500">*</span> sont obligatoires</span>
</div>
<div className="flex gap-3">
<Button
onClick={onClose}
variant="outline"
className="rounded-lg border-slate-300 hover:bg-slate-100"
>
Annuler
</Button>
<Button
onClick={handleSubmit}
className="rounded-lg bg-gradient-to-r from-indigo-600 to-blue-600 hover:from-indigo-700 hover:to-blue-700 text-white px-6"
>
<Plus className="w-4 h-4 mr-2" />
Créer {contracts.length} paie{contracts.length > 1 ? "s" : ""}
</Button>
</div>
</div>
</div>
</div>
);
}