From 4b72b4cc0dc17814e3c52a9f35ff5325199bb15c Mon Sep 17 00:00:00 2001 From: odentas Date: Mon, 13 Oct 2025 02:21:23 +0200 Subject: [PATCH] Ajout filtre org Virements salaires --- app/(app)/staff/contrats/page.tsx | 1 - app/(app)/staff/virements-salaires/page.tsx | 19 +- app/(app)/virements-salaires/page.tsx | 46 +-- app/api/contrats/[id]/generate-pdf/route.ts | 4 +- app/api/staff/contracts/bulk-esign/route.ts | 5 +- app/api/staff/contracts/search/route.ts | 9 +- .../send-esign-notification/route.ts | 2 +- app/api/staff/payslip-upload/route.ts | 141 +++++++++ app/api/staff/payslips/route.ts | 29 ++ .../staff/salary-transfers/search/route.ts | 2 + components/staff/BulkESignConfirmModal.tsx | 32 +- components/staff/ContractsGrid.tsx | 25 +- components/staff/SalaryTransfersGrid.tsx | 50 +++- components/staff/contracts/ContractEditor.tsx | 119 ++------ components/staff/contracts/PayslipCard.tsx | 273 ++++++++++++++++++ ...bulk_signature_notification_email_type.sql | 15 + 16 files changed, 633 insertions(+), 139 deletions(-) create mode 100644 app/api/staff/payslip-upload/route.ts create mode 100644 components/staff/contracts/PayslipCard.tsx create mode 100644 supabase/migrations/add_bulk_signature_notification_email_type.sql diff --git a/app/(app)/staff/contrats/page.tsx b/app/(app)/staff/contrats/page.tsx index 7eecd36..2a034a0 100644 --- a/app/(app)/staff/contrats/page.tsx +++ b/app/(app)/staff/contrats/page.tsx @@ -37,7 +37,6 @@ export default async function StaffContractsPage() { .select( `id, contract_number, employee_name, structure, type_de_contrat, start_date, end_date, created_at, etat_de_la_demande, etat_de_la_paie, dpae, gross_pay, org_id` ) - .eq("type_de_contrat", "CDD d'usage") .order("start_date", { ascending: false }) .limit(200); diff --git a/app/(app)/staff/virements-salaires/page.tsx b/app/(app)/staff/virements-salaires/page.tsx index 7db6db6..51912e5 100644 --- a/app/(app)/staff/virements-salaires/page.tsx +++ b/app/(app)/staff/virements-salaires/page.tsx @@ -43,6 +43,19 @@ export default async function StaffSalaryTransfersPage() { .order("period_month", { ascending: false }) .limit(200); + // Récupérer toutes les organisations pour le filtre + const { data: organizations, error: orgError } = await sb + .from("organizations") + .select("id, name") + .order("name", { ascending: true }); + + if (orgError) { + console.error("[staff/virements-salaires] Erreur chargement organisations:", orgError); + } + + // Debug log pour vérifier le nombre d'organisations + console.log("[staff/virements-salaires] Organizations count:", organizations?.length, "first 3:", organizations?.slice(0, 3)); + // Server-side debug logging to help diagnose empty results (will appear in Next.js server logs) try { console.log("[staff/virements-salaires] supabase fetch salary_transfers result count:", Array.isArray(salaryTransfers) ? salaryTransfers.length : typeof salaryTransfers); @@ -87,7 +100,11 @@ export default async function StaffSalaryTransfersPage() { )} {/* Client-side interactive grid */} - + ); diff --git a/app/(app)/virements-salaires/page.tsx b/app/(app)/virements-salaires/page.tsx index 112a006..2017c86 100644 --- a/app/(app)/virements-salaires/page.tsx +++ b/app/(app)/virements-salaires/page.tsx @@ -168,8 +168,8 @@ function useUserInfo() { const me = await res.json(); return { - isStaff: Boolean(me.isStaff), - orgId: me.orgId || me.active_org_id || "unknown", + isStaff: Boolean(me.is_staff || me.isStaff), + orgId: me.orgId || me.active_org_id || null, orgName: me.orgName || me.active_org_name || "Organisation", api_name: me.active_org_api_name }; @@ -182,9 +182,9 @@ function useUserInfo() { } function useOrganizations() { - const { data: userInfo } = useUserInfo(); + const { data: userInfo, isSuccess: userInfoLoaded } = useUserInfo(); - return useQuery({ + const query = useQuery({ queryKey: ["organizations"], queryFn: async () => { try { @@ -200,9 +200,17 @@ function useOrganizations() { return []; } }, - enabled: Boolean(userInfo?.isStaff), + enabled: false, staleTime: 60_000, }); + + React.useEffect(() => { + if (userInfoLoaded && userInfo?.isStaff && !query.data && !query.isFetching) { + query.refetch(); + } + }, [userInfo, userInfoLoaded, query]); + + return query; } // --- Hook pour récupérer les virements --- @@ -268,9 +276,8 @@ export default function VirementsPage() { const [copiedField, setCopiedField] = useState(null); const [selectedOrgId, setSelectedOrgId] = useState(""); - // Récupération des informations utilisateur et des organisations (pour le staff) - const { data: userInfo } = useUserInfo(); - const { data: organizations } = useOrganizations(); + const { data: userInfo, isLoading: isLoadingUser } = useUserInfo(); + const { data: organizations, isLoading: isLoadingOrgs, error: orgsError } = useOrganizations(); const queryClient = useQueryClient(); const years = useMemo(() => { @@ -384,15 +391,6 @@ export default function VirementsPage() { const clientUnpaidAll: ClientVirementItem[] = (data?.client?.unpaid ?? []) as ClientVirementItem[]; const clientRecentAll: ClientVirementItem[] = (data?.client?.recent ?? []) as ClientVirementItem[]; - // Debug logging - console.log("🔍 [virements-page] Debug data:", { - isOdentas, - itemsCount: items.length, - clientUnpaidCount: clientUnpaidAll.length, - clientRecentCount: clientRecentAll.length, - rawData: data - }); - const clientFilter = (arr: ClientVirementItem[]) => { if (!searchQuery.trim()) return arr; const q = searchQuery.toLowerCase(); @@ -521,19 +519,27 @@ export default function VirementsPage() { {/* Sélecteur d'organisation (visible uniquement par le staff) */} - {userInfo?.isStaff && organizations && organizations.length > 0 && ( + {userInfo?.isStaff && (
+ {organizations && organizations.length > 0 && ( + ({organizations.length}) + )}
)} diff --git a/app/api/contrats/[id]/generate-pdf/route.ts b/app/api/contrats/[id]/generate-pdf/route.ts index 547b5ff..8f3f9d5 100644 --- a/app/api/contrats/[id]/generate-pdf/route.ts +++ b/app/api/contrats/[id]/generate-pdf/route.ts @@ -365,8 +365,8 @@ export async function POST( cachets: { representations: contract.cachets_representations ? parseInt(contract.cachets_representations) || 0 : 0, repetitions: contract.services_repetitions ? parseInt(contract.services_repetitions) || 0 : 0, - heures: contract.nombre_d_heures ? parseInt(contract.nombre_d_heures) || 0 : 0, - heuresparjour: contract.nombre_d_heures_par_jour ? parseInt(contract.nombre_d_heures_par_jour) || 0 : 0 + heures: contract.nombre_d_heures ? parseFloat(contract.nombre_d_heures) || 0 : 0, + heuresparjour: contract.nombre_d_heures_par_jour ? parseFloat(contract.nombre_d_heures_par_jour) || 0 : 0 }, nom_responsable_traitement: orgDetails.nom_responsable_traitement || "", qualite_responsable_traitement: orgDetails.qualite_responsable_traitement || "", diff --git a/app/api/staff/contracts/bulk-esign/route.ts b/app/api/staff/contracts/bulk-esign/route.ts index 726365b..a4c9e20 100644 --- a/app/api/staff/contracts/bulk-esign/route.ts +++ b/app/api/staff/contracts/bulk-esign/route.ts @@ -107,7 +107,7 @@ export async function POST(request: NextRequest) { const { data: orgDetails } = await supabase .from('organization_details') .select('*') - .eq('organization_id', contract.org_id) + .eq('org_id', contract.org_id) .maybeSingle(); if (orgDetails) { @@ -115,6 +115,9 @@ export async function POST(request: NextRequest) { signerName = `${orgDetails.prenom_signataire || ""} ${orgDetails.nom_signataire || ""}`.trim() || signerName; organizationName = orgDetails.structure || organizationName; employerCode = orgDetails.code_employeur || employerCode; + console.log(`✅ Organisation trouvée: ${organizationName}, email: ${employerEmail}`); + } else { + console.log(`⚠️ Aucun orgDetails trouvé pour org_id: ${contract.org_id}`); } } diff --git a/app/api/staff/contracts/search/route.ts b/app/api/staff/contracts/search/route.ts index ed6159e..8693247 100644 --- a/app/api/staff/contracts/search/route.ts +++ b/app/api/staff/contracts/search/route.ts @@ -34,7 +34,14 @@ export async function GET(req: Request) { } if (employee_matricule) query = query.eq("employee_matricule", employee_matricule); if (structure) query = query.eq("structure", structure); - if (type_de_contrat) query = query.eq("type_de_contrat", type_de_contrat); + + // Handle special "RG" filter for common law contracts (CDD de droit commun + CDI) + if (type_de_contrat === "RG") { + query = query.in("type_de_contrat", ["CDD de droit commun", "CDI"]); + } else if (type_de_contrat) { + query = query.eq("type_de_contrat", type_de_contrat); + } + if (etat_de_la_demande) query = query.eq("etat_de_la_demande", etat_de_la_demande); if (etat_de_la_paie) query = query.eq("etat_de_la_paie", etat_de_la_paie); if (dpae) query = query.eq("dpae", dpae); diff --git a/app/api/staff/contracts/send-esign-notification/route.ts b/app/api/staff/contracts/send-esign-notification/route.ts index 103a3e2..a5d7260 100644 --- a/app/api/staff/contracts/send-esign-notification/route.ts +++ b/app/api/staff/contracts/send-esign-notification/route.ts @@ -63,7 +63,7 @@ export async function POST(request: NextRequest) { employerCode: employerCode, contractCount: contractCount, status: 'En attente de signature', - ctaUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'https://staging.paie.odentas.fr'}/signatures-electroniques`, + ctaUrl: 'https://paie.odentas.fr/signatures-electroniques', handlerName: 'Renaud BREVIERE-ABRAHAM', organizationId: organizationId // Ajouter pour le logging }; diff --git a/app/api/staff/payslip-upload/route.ts b/app/api/staff/payslip-upload/route.ts new file mode 100644 index 0000000..9b98b99 --- /dev/null +++ b/app/api/staff/payslip-upload/route.ts @@ -0,0 +1,141 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createSbServer } from "@/lib/supabaseServer"; +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +import { v4 as uuidv4 } from 'uuid'; + +const s3Client = new S3Client({ + region: process.env.AWS_REGION || "eu-west-3", + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID || "", + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "", + }, +}); + +const BUCKET_NAME = (process.env.AWS_S3_BUCKET || "odentas-docs").trim(); + +export async function POST(req: NextRequest) { + try { + const sb = createSbServer(); + + // Vérifier que l'utilisateur est staff + const { data: { user } } = await sb.auth.getUser(); + if (!user) { + return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); + } + + const { data: staffUser } = await sb + .from("staff_users") + .select("is_staff") + .eq("user_id", user.id) + .single(); + + if (!staffUser?.is_staff) { + return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); + } + + // Parser le form data + const formData = await req.formData(); + const file = formData.get('file') as File; + const contractId = formData.get('contract_id') as string; + const payslipId = formData.get('payslip_id') as string; + + if (!file || !contractId || !payslipId) { + return NextResponse.json({ error: "Paramètres manquants" }, { status: 400 }); + } + + // Vérifier que c'est bien un PDF + if (file.type !== 'application/pdf') { + return NextResponse.json({ error: "Seuls les fichiers PDF sont acceptés" }, { status: 400 }); + } + + // Récupérer les informations du contrat pour construire le chemin S3 + const { data: contract, error: contractError } = await sb + .from("cddu_contracts") + .select("contract_number, org_id, employee_name") + .eq("id", contractId) + .single(); + + if (contractError || !contract) { + return NextResponse.json({ error: "Contrat introuvable" }, { status: 404 }); + } + + // Récupérer les informations de la paie + const { data: payslip, error: payslipError } = await sb + .from("payslips") + .select("pay_number") + .eq("id", payslipId) + .single(); + + if (payslipError || !payslip) { + return NextResponse.json({ error: "Paie introuvable" }, { status: 404 }); + } + + // Récupérer l'organization pour avoir l'org_key + const { data: org, error: orgError } = await sb + .from("organizations") + .select("api_name") + .eq("id", contract.org_id) + .single(); + + if (orgError || !org?.api_name) { + return NextResponse.json({ error: "Organisation introuvable" }, { status: 404 }); + } + + // Générer le chemin S3: bulletins/{org_key}/contrat_{contract_number}/bulletin_paie_{pay_number}_{uuid}.pdf + const uniqueId = uuidv4().replace(/-/g, '').substring(0, 8); + const contractNumber = contract.contract_number || contractId.substring(0, 8); + const payNumber = payslip.pay_number || 'unknown'; + const filename = `bulletin_paie_${payNumber}_${uniqueId}.pdf`; + const s3Key = `bulletins/${org.api_name}/contrat_${contractNumber}/${filename}`; + + console.log('📄 [Payslip Upload] Uploading to S3:', { + contractId, + payslipId, + contractNumber, + payNumber, + s3Key, + fileSize: file.size + }); + + // Upload vers S3 + const buffer = Buffer.from(await file.arrayBuffer()); + const uploadCommand = new PutObjectCommand({ + Bucket: BUCKET_NAME, + Key: s3Key, + Body: buffer, + ContentType: 'application/pdf', + }); + + await s3Client.send(uploadCommand); + + // Mettre à jour la paie dans Supabase avec le chemin S3 + const { error: updateError } = await sb + .from('payslips') + .update({ + bulletin_pdf_url: s3Key, + bulletin_uploaded_at: new Date().toISOString(), + }) + .eq('id', payslipId); + + if (updateError) { + console.error('❌ [Payslip Upload] Erreur mise à jour Supabase:', updateError); + return NextResponse.json({ error: "Erreur lors de la mise à jour de la base de données" }, { status: 500 }); + } + + console.log('✅ [Payslip Upload] Upload réussi:', s3Key); + + return NextResponse.json({ + success: true, + s3_key: s3Key, + filename: filename, + message: "Bulletin de paie uploadé avec succès" + }); + + } catch (error) { + console.error('❌ [Payslip Upload] Erreur:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Erreur serveur" }, + { status: 500 } + ); + } +} diff --git a/app/api/staff/payslips/route.ts b/app/api/staff/payslips/route.ts index 99bd50f..6bee970 100644 --- a/app/api/staff/payslips/route.ts +++ b/app/api/staff/payslips/route.ts @@ -10,6 +10,35 @@ async function assertStaff(sb: ReturnType, userId: string return !!me?.is_staff; } +export async function GET(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 { searchParams } = new URL(req.url); + const contractId = searchParams.get('contract_id'); + + if (!contractId) { + return NextResponse.json({ error: "contract_id manquant" }, { status: 400 }); + } + + const { data: payslips, error } = await supabase + .from("payslips") + .select("*") + .eq("contract_id", contractId) + .order("pay_number", { ascending: true }); + + if (error) { + console.error('Erreur récupération payslips:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } + + return NextResponse.json({ payslips: payslips ?? [] }); +} + export async function POST(req: Request) { const supabase = createSbServer(); const { data: { user } } = await supabase.auth.getUser(); diff --git a/app/api/staff/salary-transfers/search/route.ts b/app/api/staff/salary-transfers/search/route.ts index 540b833..2306ba8 100644 --- a/app/api/staff/salary-transfers/search/route.ts +++ b/app/api/staff/salary-transfers/search/route.ts @@ -12,6 +12,7 @@ export async function GET(req: Request) { const url = new URL(req.url); const q = url.searchParams.get("q"); + const org_id = url.searchParams.get("org_id"); const period_month = url.searchParams.get("period_month"); const mode = url.searchParams.get("mode"); const notification_sent = url.searchParams.get("notification_sent"); @@ -31,6 +32,7 @@ export async function GET(req: Request) { // simple ilike search on period_label, callsheet_url, notes query = query.or(`period_label.ilike.%${q}%,callsheet_url.ilike.%${q}%,notes.ilike.%${q}%`); } + if (org_id) query = query.eq("org_id", org_id); if (period_month) query = query.eq("period_month", period_month); if (mode) query = query.eq("mode", mode); if (notification_sent !== null) { diff --git a/components/staff/BulkESignConfirmModal.tsx b/components/staff/BulkESignConfirmModal.tsx index 63f0ebe..7a81591 100644 --- a/components/staff/BulkESignConfirmModal.tsx +++ b/components/staff/BulkESignConfirmModal.tsx @@ -1,13 +1,13 @@ // components/staff/BulkESignConfirmModal.tsx "use client"; -import React from "react"; +import React, { useState } from "react"; import { X, FileSignature, AlertTriangle } from "lucide-react"; type BulkESignConfirmModalProps = { isOpen: boolean; onClose: () => void; - onConfirm: () => void; + onConfirm: (sendNotification: boolean) => void; contractCount: number; }; @@ -17,6 +17,8 @@ export default function BulkESignConfirmModal({ onConfirm, contractCount }: BulkESignConfirmModalProps) { + const [sendNotification, setSendNotification] = useState(true); + if (!isOpen) return null; return ( @@ -49,18 +51,38 @@ export default function BulkESignConfirmModal({ {/* Info */} -
+

Cette action va :

  • Créer une demande de signature électronique pour chaque contrat
  • Envoyer un email à l'employeur et au salarié pour chaque contrat
  • -
  • Envoyer un email récapitulatif au client
  • + {sendNotification &&
  • Envoyer un email récapitulatif au client
  • }

Assurez-vous que tous les contrats ont un PDF généré et que les salariés ont un email renseigné.

+ {/* Option: Envoyer notification */} +
+ +
+ {/* Actions */}
{/* Filtres rapides toujours visibles */} +
+ + + {organizations && organizations.length > 0 && ( + ({organizations.length}) + )} +
+ +
+ )} + + {/* Indicateurs en bas */} +
+ {/* Indicateur traitement */} + {!payslip.processed ? ( + + À traiter + + ) : ( + + Traitée + + )} + + {/* Indicateur virement */} + {payslip.processed && ( + payslip.transfer_done ? ( + + Virement OK + + ) : ( + + Virement en attente + + ) + )} + + {/* Indicateur AEM */} + + AEM: {payslip.aem_status || 'N/A'} + +
+
+ + ); +} diff --git a/supabase/migrations/add_bulk_signature_notification_email_type.sql b/supabase/migrations/add_bulk_signature_notification_email_type.sql new file mode 100644 index 0000000..3b2e433 --- /dev/null +++ b/supabase/migrations/add_bulk_signature_notification_email_type.sql @@ -0,0 +1,15 @@ +-- Migration: Ajouter 'bulk-signature-notification' à l'enum email_type +-- Date: 2025-10-13 +-- Description: Ajoute le type d'email pour les notifications groupées de signature électronique + +-- Ajouter la nouvelle valeur à l'enum email_type si elle n'existe pas déjà +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum + WHERE enumlabel = 'bulk-signature-notification' + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'email_type') + ) THEN + ALTER TYPE email_type ADD VALUE 'bulk-signature-notification'; + END IF; +END $$;