espace-paie-odentas/app/api/staff/virements-salaires/create/route.ts
odentas 266eb3598a feat: Implémenter store global Zustand + calcul total quantités + fix structure field + montants personnalisés virements
- Créer hook useStaffOrgSelection avec persistence localStorage
- Ajouter badge StaffOrgBadge dans Sidebar
- Synchroniser filtres org dans toutes les pages (contrats, cotisations, facturation, etc.)
- Fix calcul cachets: utiliser totalQuantities au lieu de dates.length
- Fix structure field bug: ne plus écraser avec production_name
- Ajouter création note lors modification contrat
- Implémenter montants personnalisés pour virements salaires
- Migrations SQL: custom_amount + fix_structure_field
- Réorganiser boutons ContractEditor en carte flottante droite
2025-12-01 21:51:57 +01:00

206 lines
6.4 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
// =============================================================================
// POST /api/staff/virements-salaires/create
// Creates a new salary transfer record
// =============================================================================
export async function POST(req: NextRequest) {
try {
// 1) Check auth
const supabase = createRouteHandlerClient({ cookies });
const {
data: { session },
error: sessionError,
} = await supabase.auth.getSession();
if (sessionError || !session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const user = session.user;
// 2) Check if staff
const { data: staffData, error: staffError } = await supabase
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
const isStaff = staffData?.is_staff || false;
if (!isStaff) {
return NextResponse.json(
{ error: "Forbidden: staff only" },
{ status: 403 }
);
}
// 3) Parse request body
const body = await req.json();
const {
org_id,
period_month,
period_label,
deadline,
mode,
num_appel,
total_net,
notes,
selection_mode, // 'period' (default) or 'manual'
payslip_ids, // Array of payslip IDs (required if selection_mode = 'manual')
custom_amounts, // Object mapping payslip_id to custom amount (optional)
} = body;
// 4) Validate required fields
const selectionMode = selection_mode || 'period';
if (!org_id || !deadline || !mode) {
return NextResponse.json(
{
error: "Missing required fields: org_id, deadline, mode",
},
{ status: 400 }
);
}
// Validate based on selection mode
if (selectionMode === 'period' && !period_month) {
return NextResponse.json(
{
error: "period_month is required when selection_mode is 'period'",
},
{ status: 400 }
);
}
if (selectionMode === 'manual' && (!payslip_ids || !Array.isArray(payslip_ids) || payslip_ids.length === 0)) {
return NextResponse.json(
{
error: "payslip_ids array is required and must not be empty when selection_mode is 'manual'",
},
{ status: 400 }
);
}
// 5) Verify organization exists
const { data: org, error: orgError } = await supabase
.from("organizations")
.select("id, name")
.eq("id", org_id)
.single();
if (orgError || !org) {
return NextResponse.json(
{ error: "Organization not found" },
{ status: 404 }
);
}
// 5b) If manual mode, verify all payslips exist and belong to the organization
if (selectionMode === 'manual') {
const { data: payslips, error: payslipsError } = await supabase
.from("payslips")
.select("id, organization_id, period_month")
.in("id", payslip_ids);
if (payslipsError || !payslips || payslips.length !== payslip_ids.length) {
return NextResponse.json(
{ error: "One or more payslips not found" },
{ status: 404 }
);
}
// Verify all payslips belong to the specified organization
const invalidPayslips = payslips.filter(p => p.organization_id !== org_id);
if (invalidPayslips.length > 0) {
return NextResponse.json(
{ error: `${invalidPayslips.length} payslip(s) do not belong to organization ${org_id}` },
{ status: 400 }
);
}
console.log("[create salary transfer] Manual mode: verified", payslips.length, "payslips");
}
// 6) Insert new salary transfer
const insertData = {
org_id,
period_month: selectionMode === 'period' ? period_month : null, // NULL for manual mode
period_label: period_label || null,
deadline,
mode,
num_appel: num_appel || null,
total_net: total_net || null,
notes: notes || null,
selection_mode: selectionMode,
notification_sent: false,
notification_ok: false,
salaires_payes: false,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
console.log("[create salary transfer] Insert data:", insertData);
const { data: newTransfer, error: insertError } = await supabase
.from("salary_transfers")
.insert(insertData)
.select("*, organizations!org_id(name)")
.single();
if (insertError) {
console.error("[create salary transfer] Insert error:", insertError);
return NextResponse.json(
{
error: "Failed to create salary transfer",
details: insertError.message,
code: insertError.code,
hint: insertError.hint
},
{ status: 500 }
);
}
// 6b) If manual mode, insert payslip links
if (selectionMode === 'manual' && payslip_ids && payslip_ids.length > 0) {
const links = payslip_ids.map(payslip_id => ({
salary_transfer_id: newTransfer.id,
payslip_id,
custom_amount: custom_amounts?.[payslip_id] || null,
}));
const { error: linksError } = await supabase
.from("salary_transfer_payslips")
.insert(links);
if (linksError) {
console.error("[create salary transfer] Links insert error:", linksError);
// Rollback: delete the transfer we just created
await supabase.from("salary_transfers").delete().eq("id", newTransfer.id);
return NextResponse.json(
{
error: "Failed to link payslips to salary transfer",
details: linksError.message
},
{ status: 500 }
);
}
console.log("[create salary transfer] Linked", payslip_ids.length, "payslips to transfer");
}
// 7) Return the new record
return NextResponse.json({
success: true,
data: newTransfer,
payslips_count: selectionMode === 'manual' ? payslip_ids?.length : null,
});
} catch (err: any) {
console.error("Error in create salary transfer:", err);
return NextResponse.json(
{ error: err.message || "Internal server error" },
{ status: 500 }
);
}
}