- Ajout détails cachets/répétitions/heures au modal ContractDetails - Card verte avec validation quand tous les contrats ont une fiche de paie - Système complet de création de fiches de paie avec recherche et vérification - Modal liste des contrats sans paie avec création directe - Amélioration édition dates dans PayslipDetailsModal - Optimisation recherche contrats (ordre des filtres) - Augmentation limite pagination ContractsGrid à 200 - Ajout logs debug génération PDF logo - Script SQL vérification cohérence structure/organisation
691 lines
27 KiB
TypeScript
691 lines
27 KiB
TypeScript
// components/staff/CreatePayslipModal.tsx
|
|
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { X, FileText, Search, Loader, AlertCircle } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
type Contract = {
|
|
id: string;
|
|
contract_number?: string | null;
|
|
employee_name?: string | null;
|
|
structure?: string | null;
|
|
type_de_contrat?: string | null;
|
|
start_date?: string | null;
|
|
end_date?: string | null;
|
|
production_name?: string | null;
|
|
n_objet?: string | null;
|
|
objet_spectacle?: string | null;
|
|
salaries?: {
|
|
salarie?: string | null;
|
|
nom?: string | null;
|
|
prenom?: string | null;
|
|
} | null;
|
|
organizations?: {
|
|
name?: string | null;
|
|
} | null;
|
|
};
|
|
|
|
type CreatePayslipModalProps = {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onPayslipCreated?: () => void;
|
|
preselectedContractId?: string | null;
|
|
};
|
|
|
|
function formatDate(dateString: string | null | undefined): string {
|
|
if (!dateString) return "—";
|
|
try {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('fr-FR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
} catch {
|
|
return "—";
|
|
}
|
|
}
|
|
|
|
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(' ');
|
|
}
|
|
|
|
if (contract.employee_name) {
|
|
return contract.employee_name;
|
|
}
|
|
|
|
return "—";
|
|
}
|
|
|
|
export default function CreatePayslipModal({
|
|
isOpen,
|
|
onClose,
|
|
onPayslipCreated,
|
|
preselectedContractId
|
|
}: CreatePayslipModalProps) {
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [contracts, setContracts] = useState<Contract[]>([]);
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
const [selectedContract, setSelectedContract] = useState<Contract | null>(null);
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
const [isMultiMonth, setIsMultiMonth] = useState(false);
|
|
const [suggestedPayNumber, setSuggestedPayNumber] = useState<number | null>(null);
|
|
const [hasExistingPayslip, setHasExistingPayslip] = useState(false);
|
|
const [existingPayslipMessage, setExistingPayslipMessage] = useState("");
|
|
const [isCheckingExisting, setIsCheckingExisting] = useState(false);
|
|
const [isLoadingPreselected, setIsLoadingPreselected] = useState(false);
|
|
|
|
// Form fields
|
|
const [periodStart, setPeriodStart] = useState("");
|
|
const [periodEnd, setPeriodEnd] = useState("");
|
|
const [payNumber, setPayNumber] = useState("");
|
|
const [grossAmount, setGrossAmount] = useState("");
|
|
const [netAmount, setNetAmount] = useState("");
|
|
const [netAfterWithholding, setNetAfterWithholding] = useState("");
|
|
const [employerCost, setEmployerCost] = useState("");
|
|
|
|
// Load preselected contract
|
|
useEffect(() => {
|
|
if (!isOpen || !preselectedContractId) return;
|
|
|
|
const loadPreselectedContract = async () => {
|
|
setIsLoadingPreselected(true);
|
|
try {
|
|
// Fetch contract by ID directly
|
|
const response = await fetch(`/api/staff/contracts/${preselectedContractId}`);
|
|
if (!response.ok) {
|
|
throw new Error('Erreur lors du chargement du contrat');
|
|
}
|
|
const data = await response.json();
|
|
const contract = data.contract || data;
|
|
|
|
// Set the selected contract
|
|
setSelectedContract(contract);
|
|
setSearchQuery("");
|
|
setContracts([]);
|
|
setHasExistingPayslip(false);
|
|
setExistingPayslipMessage("");
|
|
|
|
// Déterminer si le contrat est multi-mois
|
|
if (contract.start_date && contract.end_date) {
|
|
const startDate = new Date(contract.start_date);
|
|
const endDate = new Date(contract.end_date);
|
|
const isMulti = startDate.getMonth() !== endDate.getMonth() ||
|
|
startDate.getFullYear() !== endDate.getFullYear();
|
|
setIsMultiMonth(isMulti);
|
|
|
|
// Si multi-mois, récupérer le dernier numéro de paie
|
|
if (isMulti) {
|
|
try {
|
|
const payNumResponse = await fetch(`/api/staff/payslips/last-pay-number?contract_id=${contract.id}`);
|
|
if (payNumResponse.ok) {
|
|
const payNumData = await payNumResponse.json();
|
|
const nextNumber = (payNumData.last_pay_number || 0) + 1;
|
|
setSuggestedPayNumber(nextNumber);
|
|
setPayNumber(String(nextNumber));
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching last pay number:', err);
|
|
}
|
|
} else {
|
|
// Si mono-mois, vérifier si une paie existe déjà
|
|
setIsCheckingExisting(true);
|
|
try {
|
|
const checkResponse = await fetch(`/api/staff/payslips/check-existing?contract_id=${contract.id}`);
|
|
if (checkResponse.ok) {
|
|
const checkData = await checkResponse.json();
|
|
if (checkData.has_existing) {
|
|
setHasExistingPayslip(true);
|
|
setExistingPayslipMessage(checkData.message || "Une fiche de paie existe déjà pour ce contrat");
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error checking existing payslip:', err);
|
|
} finally {
|
|
setIsCheckingExisting(false);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading preselected contract:', error);
|
|
toast.error('Erreur lors du chargement du contrat');
|
|
} finally {
|
|
setIsLoadingPreselected(false);
|
|
}
|
|
};
|
|
|
|
loadPreselectedContract();
|
|
}, [isOpen, preselectedContractId]);
|
|
|
|
// Search contracts
|
|
useEffect(() => {
|
|
if (!searchQuery.trim() || !isOpen) {
|
|
setContracts([]);
|
|
return;
|
|
}
|
|
|
|
const timer = setTimeout(async () => {
|
|
setIsSearching(true);
|
|
try {
|
|
const response = await fetch(`/api/staff/payslips/search-contracts?q=${encodeURIComponent(searchQuery)}`);
|
|
if (!response.ok) {
|
|
throw new Error('Erreur lors de la recherche');
|
|
}
|
|
const data = await response.json();
|
|
setContracts(data.contracts || []);
|
|
} catch (error) {
|
|
console.error('Error searching contracts:', error);
|
|
toast.error('Erreur lors de la recherche des contrats');
|
|
setContracts([]);
|
|
} finally {
|
|
setIsSearching(false);
|
|
}
|
|
}, 300);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [searchQuery, isOpen]);
|
|
|
|
// Reset form when modal closes
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setSearchQuery("");
|
|
setContracts([]);
|
|
setSelectedContract(null);
|
|
setIsMultiMonth(false);
|
|
setSuggestedPayNumber(null);
|
|
setHasExistingPayslip(false);
|
|
setExistingPayslipMessage("");
|
|
setIsCheckingExisting(false);
|
|
setPeriodStart("");
|
|
setPeriodEnd("");
|
|
setPayNumber("");
|
|
setGrossAmount("");
|
|
setNetAmount("");
|
|
setNetAfterWithholding("");
|
|
setEmployerCost("");
|
|
}
|
|
}, [isOpen]);
|
|
|
|
// Keyboard navigation
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, onClose]);
|
|
|
|
const handleSelectContract = (contract: Contract) => {
|
|
setSelectedContract(contract);
|
|
setSearchQuery("");
|
|
setContracts([]);
|
|
setHasExistingPayslip(false);
|
|
setExistingPayslipMessage("");
|
|
|
|
// Déterminer si le contrat est multi-mois
|
|
if (contract.start_date && contract.end_date) {
|
|
const startDate = new Date(contract.start_date);
|
|
const endDate = new Date(contract.end_date);
|
|
const isMulti = startDate.getMonth() !== endDate.getMonth() ||
|
|
startDate.getFullYear() !== endDate.getFullYear();
|
|
setIsMultiMonth(isMulti);
|
|
|
|
// Si multi-mois, récupérer le dernier numéro de paie
|
|
if (isMulti) {
|
|
fetchLastPayNumber(contract.id);
|
|
} else {
|
|
// Si mono-mois, vérifier si une paie existe déjà
|
|
checkExistingPayslip(contract.id);
|
|
}
|
|
}
|
|
};
|
|
|
|
const checkExistingPayslip = async (contractId: string) => {
|
|
setIsCheckingExisting(true);
|
|
try {
|
|
const response = await fetch(`/api/staff/payslips/check-existing?contract_id=${contractId}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.has_existing) {
|
|
setHasExistingPayslip(true);
|
|
setExistingPayslipMessage(data.message || "Une fiche de paie existe déjà pour ce contrat");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking existing payslip:', error);
|
|
} finally {
|
|
setIsCheckingExisting(false);
|
|
}
|
|
};
|
|
|
|
const fetchLastPayNumber = async (contractId: string) => {
|
|
try {
|
|
const response = await fetch(`/api/staff/payslips/last-pay-number?contract_id=${contractId}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const nextNumber = (data.last_pay_number || 0) + 1;
|
|
setSuggestedPayNumber(nextNumber);
|
|
setPayNumber(String(nextNumber));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching last pay number:', error);
|
|
}
|
|
};
|
|
|
|
const handleCreatePayslip = async (closeAfter: boolean = true) => {
|
|
if (!selectedContract) {
|
|
toast.error('Veuillez sélectionner un contrat');
|
|
return;
|
|
}
|
|
|
|
if (!periodStart || !periodEnd) {
|
|
toast.error('Veuillez renseigner les dates de période');
|
|
return;
|
|
}
|
|
|
|
if (isMultiMonth && !payNumber) {
|
|
toast.error('Veuillez renseigner le numéro de paie');
|
|
return;
|
|
}
|
|
|
|
setIsCreating(true);
|
|
try {
|
|
const response = await fetch('/api/staff/payslips/create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
contract_id: selectedContract.id,
|
|
period_start: periodStart,
|
|
period_end: periodEnd,
|
|
pay_number: isMultiMonth && payNumber ? parseInt(payNumber) : undefined,
|
|
gross_amount: grossAmount ? parseFloat(grossAmount) : null,
|
|
net_amount: netAmount ? parseFloat(netAmount) : null,
|
|
net_after_withholding: netAfterWithholding ? parseFloat(netAfterWithholding) : null,
|
|
employer_cost: employerCost ? parseFloat(employerCost) : null,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Erreur lors de la création');
|
|
}
|
|
|
|
toast.success('Fiche de paie créée avec succès');
|
|
|
|
if (closeAfter) {
|
|
onPayslipCreated?.();
|
|
onClose();
|
|
} else {
|
|
// Réinitialiser le formulaire mais garder le modal ouvert
|
|
onPayslipCreated?.();
|
|
setSelectedContract(null);
|
|
setSearchQuery("");
|
|
setContracts([]);
|
|
setIsMultiMonth(false);
|
|
setSuggestedPayNumber(null);
|
|
setHasExistingPayslip(false);
|
|
setExistingPayslipMessage("");
|
|
setPeriodStart("");
|
|
setPeriodEnd("");
|
|
setPayNumber("");
|
|
setGrossAmount("");
|
|
setNetAmount("");
|
|
setNetAfterWithholding("");
|
|
setEmployerCost("");
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating payslip:', error);
|
|
toast.error(error instanceof Error ? error.message : 'Erreur lors de la création de la fiche de paie');
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg w-full max-w-4xl max-h-[90vh] flex flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="size-5 text-blue-600" />
|
|
<h2 className="text-lg font-semibold">Créer une fiche de paie</h2>
|
|
</div>
|
|
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
|
>
|
|
<X className="size-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
{isLoadingPreselected ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="text-center">
|
|
<Loader className="w-8 h-8 text-blue-600 animate-spin mx-auto mb-3" />
|
|
<p className="text-sm text-gray-600">Chargement du contrat...</p>
|
|
</div>
|
|
</div>
|
|
) : !selectedContract ? (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Rechercher un contrat
|
|
</label>
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Nom salarié, n° contrat, production, organisation..."
|
|
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
autoFocus
|
|
/>
|
|
{isSearching && (
|
|
<Loader className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-400 animate-spin" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Search results */}
|
|
{contracts.length > 0 && (
|
|
<div className="mt-3 border rounded-lg overflow-hidden max-h-96 overflow-y-auto">
|
|
{contracts.map((contract) => (
|
|
<button
|
|
key={contract.id}
|
|
onClick={() => handleSelectContract(contract)}
|
|
className="w-full text-left p-4 hover:bg-blue-50 border-b last:border-b-0 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium text-gray-900">
|
|
{formatEmployeeName(contract)}
|
|
</div>
|
|
<div className="text-sm text-gray-600 mt-1">
|
|
{contract.contract_number && (
|
|
<span className="mr-3">N° {contract.contract_number}</span>
|
|
)}
|
|
{contract.structure && (
|
|
<span className="mr-3">{contract.structure}</span>
|
|
)}
|
|
{contract.organizations?.name && (
|
|
<span className="mr-3">{contract.organizations.name}</span>
|
|
)}
|
|
</div>
|
|
{contract.production_name && (
|
|
<div className="text-sm text-gray-500 mt-1">
|
|
Production: {contract.production_name}
|
|
</div>
|
|
)}
|
|
{(contract.n_objet || contract.objet_spectacle) && (
|
|
<div className="text-sm text-gray-500">
|
|
N° Objet: {contract.n_objet || contract.objet_spectacle}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="text-sm text-gray-500 whitespace-nowrap">
|
|
{formatDate(contract.start_date)} - {formatDate(contract.end_date)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{searchQuery.trim() && !isSearching && contracts.length === 0 && (
|
|
<div className="mt-3 p-4 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800 flex items-start gap-2">
|
|
<AlertCircle className="size-4 mt-0.5 flex-shrink-0" />
|
|
<p>Aucun contrat trouvé pour cette recherche</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div>
|
|
{/* Selected contract info */}
|
|
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1">
|
|
<h3 className="font-medium text-gray-900">
|
|
{formatEmployeeName(selectedContract)}
|
|
</h3>
|
|
<div className="text-sm text-gray-600 mt-1">
|
|
{selectedContract.contract_number && (
|
|
<span className="mr-3">N° {selectedContract.contract_number}</span>
|
|
)}
|
|
{selectedContract.structure && (
|
|
<span className="mr-3">{selectedContract.structure}</span>
|
|
)}
|
|
{selectedContract.type_de_contrat && (
|
|
<span className="mr-3">{selectedContract.type_de_contrat === "CDD d'usage" ? "CDDU" : selectedContract.type_de_contrat}</span>
|
|
)}
|
|
{isMultiMonth && (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-800">
|
|
Contrat multi-mois
|
|
</span>
|
|
)}
|
|
</div>
|
|
{selectedContract.production_name && (
|
|
<div className="text-sm text-gray-600 mt-1">
|
|
Production: {selectedContract.production_name}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => setSelectedContract(null)}
|
|
className="text-sm text-blue-600 hover:text-blue-800"
|
|
>
|
|
Changer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Alerte si une paie existe déjà */}
|
|
{isCheckingExisting && (
|
|
<div className="mb-4 p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center gap-2">
|
|
<Loader className="size-4 animate-spin text-gray-600" />
|
|
<p className="text-sm text-gray-600">Vérification des paies existantes...</p>
|
|
</div>
|
|
)}
|
|
|
|
{hasExistingPayslip && (
|
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
<div className="flex items-start gap-2">
|
|
<AlertCircle className="size-5 text-red-600 mt-0.5 flex-shrink-0" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-red-800">
|
|
Impossible de créer une paie
|
|
</p>
|
|
<p className="text-sm text-red-700 mt-1">
|
|
{existingPayslipMessage}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form fields */}
|
|
{!hasExistingPayslip && (
|
|
<div className="space-y-6">
|
|
{/* Section Numéro de paie (si multi-mois) */}
|
|
{isMultiMonth && (
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-slate-900 border-b pb-2">
|
|
Numéro de paie
|
|
</h3>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-600 mb-1">
|
|
N° de paie <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
value={payNumber}
|
|
onChange={(e) => setPayNumber(e.target.value)}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
placeholder={suggestedPayNumber ? `Suggestion: ${suggestedPayNumber}` : "1"}
|
|
required
|
|
/>
|
|
{suggestedPayNumber && (
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
Numéro suggéré basé sur les paies existantes: {suggestedPayNumber}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Section Dates */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-slate-900 border-b pb-2">
|
|
Période et dates
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-600 mb-1">
|
|
Date début période <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={periodStart}
|
|
onChange={(e) => setPeriodStart(e.target.value)}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-600 mb-1">
|
|
Date fin période <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={periodEnd}
|
|
onChange={(e) => setPeriodEnd(e.target.value)}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section Montants */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-semibold text-slate-900 border-b pb-2">
|
|
Montants
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-600 mb-1">
|
|
Salaire brut
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={grossAmount}
|
|
onChange={(e) => setGrossAmount(e.target.value)}
|
|
placeholder="0.00"
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-600 mb-1">
|
|
Net avant PAS
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={netAmount}
|
|
onChange={(e) => setNetAmount(e.target.value)}
|
|
placeholder="0.00"
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-600 mb-1">
|
|
Net à payer
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={netAfterWithholding}
|
|
onChange={(e) => setNetAfterWithholding(e.target.value)}
|
|
placeholder="0.00"
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-600 mb-1">
|
|
Coût employeur
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={employerCost}
|
|
onChange={(e) => setEmployerCost(e.target.value)}
|
|
placeholder="0.00"
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-3 p-4 border-t bg-gray-50">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
disabled={isCreating}
|
|
>
|
|
Annuler
|
|
</button>
|
|
{selectedContract && !hasExistingPayslip && (
|
|
<>
|
|
<button
|
|
onClick={() => handleCreatePayslip(false)}
|
|
disabled={isCreating || !periodStart || !periodEnd || isCheckingExisting}
|
|
className="px-4 py-2 text-sm font-medium text-blue-600 bg-white border border-blue-600 rounded-lg hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{isCreating && <Loader className="size-4 animate-spin" />}
|
|
Créer et ajouter une autre paie
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => handleCreatePayslip(true)}
|
|
disabled={isCreating || !periodStart || !periodEnd || isCheckingExisting}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
{isCreating && <Loader className="size-4 animate-spin" />}
|
|
Créer et fermer
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|