Files
GTARH/src/pages/ExportPaie.tsx
2025-12-02 17:57:33 +01:00

564 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ArrowLeft, Download, FileSpreadsheet } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { toast } from "sonner";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import ExcelJS from 'exceljs';
interface CongeDetail {
nb: number;
dates: string;
}
interface AutreConge {
type: string;
nb: number;
dates: string;
}
interface DataPaie {
employe: string;
email: string;
service: string;
rtt: CongeDetail;
cp: CongeDetail;
aap: CongeDetail;
am: CongeDetail;
autres: AutreConge[];
}
const ExportPaie = () => {
const navigate = useNavigate();
const [mois, setMois] = useState(new Date().getMonth() + 1);
const [annee, setAnnee] = useState(new Date().getFullYear());
const [dataPaie, setDataPaie] = useState<DataPaie[]>([]);
const [loading, setLoading] = useState(false);
const moisOptions = [
{ value: 1, label: 'Janvier' }, { value: 2, label: 'Février' },
{ value: 3, label: 'Mars' }, { value: 4, label: 'Avril' },
{ value: 5, label: 'Mai' }, { value: 6, label: 'Juin' },
{ value: 7, label: 'Juillet' }, { value: 8, label: 'Août' },
{ value: 9, label: 'Septembre' }, { value: 10, label: 'Octobre' },
{ value: 11, label: 'Novembre' }, { value: 12, label: 'Décembre' }
];
const genererRapport = async () => {
setLoading(true);
try {
const token = localStorage.getItem('token');
const response = await fetch(
`/api/export/paie?mois=${mois}&annee=${annee}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const data = await response.json();
console.log('📊 Données reçues:', data);
setDataPaie(data);
toast.success(`Rapport généré avec succès (${data.length} employé${data.length > 1 ? 's' : ''})`);
} catch (error) {
console.error('❌ Erreur:', error);
toast.error("Erreur lors de la génération du rapport");
} finally {
setLoading(false);
}
};
const exporterExcel = async () => {
if (dataPaie.length === 0) return toast.error("Aucune donnée à exporter");
const moisLabel = moisOptions.find(m => m.value === mois)?.label || 'Inconnu';
try {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet(`Congés ${moisLabel}`);
// ✅ MODIFICATION : Colonnes sans Email
worksheet.columns = [
{ header: 'Collaborateur', key: 'collaborateur', width: 20 },
{ header: 'Service', key: 'service', width: 20 },
{ header: 'RTT Nb', key: 'rtt_nb', width: 10 },
{ header: 'RTT Dates', key: 'rtt_dates', width: 25 },
{ header: 'CP Nb', key: 'cp_nb', width: 10 },
{ header: 'CP Dates', key: 'cp_dates', width: 25 },
{ header: 'AAP Nb', key: 'aap_nb', width: 10 },
{ header: 'AAP Dates', key: 'aap_dates', width: 25 },
{ header: 'AM Nb', key: 'am_nb', width: 10 },
{ header: 'AM Dates', key: 'am_dates', width: 25 },
{ header: 'Autres Type', key: 'autres_type', width: 20 },
{ header: 'Autres Nb', key: 'autres_nb', width: 10 },
{ header: 'Autres Dates', key: 'autres_dates', width: 30 }
];
// Style de l'en-tête
worksheet.getRow(1).height = 35;
worksheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' }, size: 12 };
worksheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
worksheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF7e5aa2' }
};
// Bordures ÉPAISSES pour l'en-tête
worksheet.getRow(1).eachCell((cell) => {
cell.border = {
top: { style: 'medium', color: { argb: 'FF000000' } },
left: { style: 'medium', color: { argb: 'FF000000' } },
bottom: { style: 'medium', color: { argb: 'FF000000' } },
right: { style: 'medium', color: { argb: 'FF000000' } }
};
});
let currentRow = 2;
// Ajouter les données avec fusion de cellules
dataPaie.forEach((row, employeIndex) => {
const startRow = currentRow;
// ✅ MODIFICATION : Extraire uniquement le nom (dernier mot)
const nomComplet = row.employe.split(' ');
const nomSeul = nomComplet[nomComplet.length - 1]; // Prend le dernier mot (le nom)
// Calculer le nombre maximum de lignes nécessaires pour cet employé
const rttDatesArray = row.rtt?.dates ? row.rtt.dates.split(' ; ') : [];
const cpDatesArray = row.cp?.dates ? row.cp.dates.split(' ; ') : [];
const aapDatesArray = row.aap?.dates ? row.aap.dates.split(' ; ') : [];
const amDatesArray = row.am?.dates ? row.am.dates.split(' ; ') : [];
// Gérer les "Autres"
let autresDatesArray: string[][] = [];
if (row.autres && row.autres.length > 0) {
autresDatesArray = row.autres.map(a => a.dates.split(' ; '));
}
const maxAutresDates = Math.max(...autresDatesArray.map(arr => arr.length), 1);
const maxRows = Math.max(
rttDatesArray.length || 1,
cpDatesArray.length || 1,
aapDatesArray.length || 1,
amDatesArray.length || 1,
maxAutresDates
);
// Ajouter les lignes nécessaires
for (let i = 0; i < maxRows; i++) {
const rowData: any = {};
// Première ligne : inclure nom et service (sans email)
if (i === 0) {
rowData.collaborateur = nomSeul; // ✅ Uniquement le nom
rowData.service = row.service;
rowData.rtt_nb = row.rtt?.nb || 0;
rowData.cp_nb = row.cp?.nb || 0;
rowData.aap_nb = row.aap?.nb || 0;
rowData.am_nb = row.am?.nb || 0;
// Autres (première ligne)
if (row.autres && row.autres.length > 0) {
rowData.autres_type = row.autres.map(a => a.type).join('\n');
rowData.autres_nb = row.autres.map(a => a.nb).join('\n');
}
} else {
rowData.collaborateur = '';
rowData.service = '';
rowData.rtt_nb = '';
rowData.cp_nb = '';
rowData.aap_nb = '';
rowData.am_nb = '';
rowData.autres_type = '';
rowData.autres_nb = '';
}
// Dates RTT
rowData.rtt_dates = rttDatesArray[i] || '';
// Dates CP
rowData.cp_dates = cpDatesArray[i] || '';
// Dates AAP
rowData.aap_dates = aapDatesArray[i] || '';
// Dates AM
rowData.am_dates = amDatesArray[i] || '';
// Dates Autres
if (row.autres && row.autres.length > 0) {
const allAutresDates = row.autres.map(a => {
const dates = a.dates.split(' ; ');
return dates[i] || '';
}).filter(d => d !== '').join(' | ');
rowData.autres_dates = allAutresDates;
} else {
rowData.autres_dates = '';
}
worksheet.addRow(rowData);
currentRow++;
}
const endRow = currentRow - 1;
// ✅ MODIFICATION : Fusionner les cellules (ajusté pour la nouvelle structure)
if (maxRows > 1) {
worksheet.mergeCells(`A${startRow}:A${endRow}`); // Collaborateur
worksheet.mergeCells(`B${startRow}:B${endRow}`); // Service
worksheet.mergeCells(`C${startRow}:C${endRow}`); // RTT Nb
worksheet.mergeCells(`E${startRow}:E${endRow}`); // CP Nb
worksheet.mergeCells(`G${startRow}:G${endRow}`); // AAP Nb
worksheet.mergeCells(`I${startRow}:I${endRow}`); // AM Nb
worksheet.mergeCells(`K${startRow}:K${endRow}`); // Autres Type
worksheet.mergeCells(`L${startRow}:L${endRow}`); // Autres Nb
}
// Appliquer les styles pour ce groupe de lignes
for (let r = startRow; r <= endRow; r++) {
const currentRowObj = worksheet.getRow(r);
currentRowObj.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
// Hauteur de ligne
currentRowObj.height = 25;
// ✅ MODIFICATION : Couleurs ajustées (colonnes décalées)
currentRowObj.getCell(3).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE9D5FF' } }; // RTT Nb
currentRowObj.getCell(4).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE9D5FF' } }; // RTT Dates
currentRowObj.getCell(5).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFDBEAFE' } }; // CP Nb
currentRowObj.getCell(6).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFDBEAFE' } }; // CP Dates
currentRowObj.getCell(7).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD1FAE5' } }; // AAP Nb
currentRowObj.getCell(8).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD1FAE5' } }; // AAP Dates
currentRowObj.getCell(9).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEE2E2' } }; // AM Nb
currentRowObj.getCell(10).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEE2E2' } }; // AM Dates
currentRowObj.getCell(11).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } }; // Autres Type
currentRowObj.getCell(12).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } }; // Autres Nb
currentRowObj.getCell(13).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFEF3C7' } }; // Autres Dates
// Bordures ÉPAISSES pour toutes les cellules
currentRowObj.eachCell((cell) => {
cell.border = {
top: { style: 'medium', color: { argb: 'FF000000' } },
left: { style: 'medium', color: { argb: 'FF000000' } },
bottom: { style: 'medium', color: { argb: 'FF000000' } },
right: { style: 'medium', color: { argb: 'FF000000' } }
};
});
}
});
// Générer le fichier Excel
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `conges_paie_${moisLabel}_${annee}.xlsx`;
link.click();
toast.success("Export Excel réussi avec fusion de cellules !");
} catch (error) {
console.error('Erreur export Excel:', error);
toast.error("Erreur lors de l'export Excel");
}
};
const exporterCSV = () => {
if (dataPaie.length === 0) return toast.error("Aucune donnée à exporter");
const headers = [
'Employé', 'Email', 'Service',
'RTT Nb', 'RTT Dates',
'CP Nb', 'CP Dates',
'AAP Nb', 'AAP Dates',
'AM Nb', 'AM Dates',
'Autres Type', 'Autres Nb', 'Autres Dates'
];
const rows = dataPaie.map(row => {
const baseData = [
row.employe,
row.email,
row.service,
row.rtt?.nb || 0,
row.rtt?.dates || '',
row.cp?.nb || 0,
row.cp?.dates || '',
row.aap?.nb || 0,
row.aap?.dates || '',
row.am?.nb || 0,
row.am?.dates || ''
];
if (row.autres && row.autres.length > 0) {
const autresTypes = row.autres.map(a => a.type).join(' | ');
const autresNb = row.autres.map(a => a.nb).join(' | ');
const autresDates = row.autres.map(a => a.dates).join(' | ');
return [...baseData, autresTypes, autresNb, autresDates];
} else {
return [...baseData, '', '', ''];
}
});
const csvContent = [
headers.join(';'),
...rows.map(row => row.join(';'))
].join('\n');
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `conges_paie_${moisOptions.find(m => m.value === mois)?.label}_${annee}.csv`;
link.click();
toast.success("Export CSV réussi");
};
return (
<div className="min-h-screen bg-gradient-subtle">
<header className="bg-card border-b border-border shadow-card">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button variant="ghost" size="sm" onClick={() => navigate("/dashboard")}>
<ArrowLeft className="w-4 h-4 mr-2" />
Retour
</Button>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}>
<FileSpreadsheet className="w-5 h-5 text-primary-foreground" />
</div>
<h1 className="text-2xl font-bold text-foreground">Export pour fiche de paie</h1>
</div>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Card className="shadow-elegant border-0 mb-8">
<CardHeader>
<CardTitle>Paramètres d'export</CardTitle>
<CardDescription>Sélectionnez la période pour générer le rapport</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<Label>Mois</Label>
<Select value={mois.toString()} onValueChange={(value) => setMois(parseInt(value))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{moisOptions.map(option => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Année</Label>
<Select value={annee.toString()} onValueChange={(value) => setAnnee(parseInt(value))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{[2023, 2024, 2025, 2026].map(year => (
<SelectItem key={year} value={year.toString()}>{year}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Bouton Générer le rapport toujours affiché avec fond mauve */}
<div className="flex items-end">
<Button
onClick={genererRapport}
disabled={loading}
className="w-full bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
>
{loading ? 'Génération...' : 'Générer le rapport'}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Bouton Export Excel toujours affiché, grisé sil ny a pas de données */}
<div className="flex gap-4 mb-6">
<Button
onClick={exporterExcel}
disabled={dataPaie.length === 0}
variant="default"
className="bg-[#7e5aa2] hover:bg-[#6b4891] text-white transition-smooth shadow-elegant"
>
<Download className="w-4 h-4 mr-2" />Export Excel (avec couleurs)
</Button>
</div>
{/* Tableau des données uniquement si data présente */}
{dataPaie.length > 0 ? (
<Card className="shadow-card border-0">
<CardHeader>
<CardTitle>Rapport des congés - {moisOptions.find(m => m.value === mois)?.label} {annee}</CardTitle>
<CardDescription>
AAP = Absence autorisée payée (ex : enfant malade, récup de JPOFS, etc) • AM = Arrêt maladie
</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<div className="border-[2px] border-black rounded-lg overflow-hidden">
<table className="w-full border-collapse">
<thead>
<tr className="border-b-[2px] border-black bg-gray-100">
<th className="font-bold text-black border-r-[2px] border-black p-4 text-left">
Collaborateur
</th>
<th className="font-bold text-black border-r-[2px] border-black p-4 text-left">
Service
</th>
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
RTT<br />
<span className="text-xs font-normal">(Nb / Dates)</span>
</th>
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
CP<br />
<span className="text-xs font-normal">(Nb / Dates)</span>
</th>
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
AAP<br />
<span className="text-xs font-normal">(Nb / Dates)</span>
</th>
<th className="font-bold text-black border-r-[2px] border-black p-4 text-center">
AM<br />
<span className="text-xs font-normal">(Nb / Dates)</span>
</th>
<th className="font-bold text-black p-4 text-center">
Autres<br />
<span className="text-xs font-normal">(Type / Nb / Dates)</span>
</th>
</tr>
</thead>
<tbody>
{dataPaie.map((row, index) => (
<tr
key={index}
className="border-b-[2px] border-black last:border-b-0 hover:bg-gray-50"
>
<td className="font-medium border-r-[2px] border-black p-4 align-top bg-white">
{row.employe}
</td>
<td className="border-r-[2px] border-black p-4 align-top bg-white">
{row.service}
</td>
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
{row.rtt && row.rtt.nb > 0 ? (
<div className="space-y-2">
<div className="font-semibold text-lg">{row.rtt.nb}</div>
<div className="text-xs text-gray-600 space-y-1">
{row.rtt.dates.split(' ; ').map((date, idx) => (
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
{date}
</div>
))}
</div>
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
{row.cp && row.cp.nb > 0 ? (
<div className="space-y-2">
<div className="font-semibold text-lg">{row.cp.nb}</div>
<div className="text-xs text-gray-600 space-y-1">
{row.cp.dates.split(' ; ').map((date, idx) => (
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
{date}
</div>
))}
</div>
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
{row.aap && row.aap.nb > 0 ? (
<div className="space-y-2">
<div className="font-semibold text-lg">{row.aap.nb}</div>
<div className="text-xs text-gray-600 space-y-1">
{row.aap.dates.split(' ; ').map((date, idx) => (
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
{date}
</div>
))}
</div>
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="text-center bg-white border-r-[2px] border-black p-4 align-top">
{row.am && row.am.nb > 0 ? (
<div className="space-y-2">
<div className="font-semibold text-lg">{row.am.nb}</div>
<div className="text-xs text-gray-600 space-y-1">
{row.am.dates.split(' ; ').map((date, idx) => (
<div key={idx} className="py-1 border-t border-gray-300 first:border-t-0">
{date}
</div>
))}
</div>
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
<td className="text-center bg-white p-4 align-top">
{row.autres && row.autres.length > 0 ? (
<div className="space-y-3">
{row.autres.map((autre, idx) => (
<div key={idx} className="space-y-2 pb-3 border-b-[2px] border-gray-300 last:border-0 last:pb-0">
<div className="text-xs font-medium text-gray-700">{autre.type}</div>
<div className="font-semibold text-lg">{autre.nb}</div>
<div className="text-xs text-gray-600 space-y-1">
{autre.dates.split(' ; ').map((date, dateIdx) => (
<div key={dateIdx} className="py-1 border-t border-gray-300 first:border-t-0">
{date}
</div>
))}
</div>
</div>
))}
</div>
) : (
<span className="text-gray-400">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</CardContent>
</Card>
) : (
<Card className="shadow-card border-0">
<CardContent className="p-12 text-center">
<FileSpreadsheet className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
<p className="text-muted-foreground text-lg">Sélectionnez une période et cliquez sur "Générer le rapport"</p>
</CardContent>
</Card>
)}
</main>
</div>
);
};
export default ExportPaie;