Added Budget Page

This commit is contained in:
2026-02-15 15:05:59 +01:00
parent 5185a41bb6
commit 5772a358f5
10 changed files with 905 additions and 11 deletions

161
app/api/budget/route.ts Normal file
View File

@@ -0,0 +1,161 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth';
// GET - Liste tous les prescripteurs avec budgets, consommé et restant
// PATCH - Met à jour le budget d'un prescripteur (body: { prescripteurValue, montant })
export async function GET() {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const [prescripteurs, budgets, participations] = await Promise.all([
prisma.adherentOption.findMany({
where: { type: 'prescripteur' },
orderBy: [{ order: 'asc' }, { value: 'asc' }],
}),
prisma.prescripteurBudget.findMany(),
prisma.participationFinanciere.findMany({
include: {
adherent: { select: { prescripteur: true, prenom: true, nom: true } },
trajet: { select: { date: true } },
},
orderBy: { createdAt: 'desc' },
}),
]);
const consomméMap = new Map<string, number>();
const historiqueMap = new Map<string, Array<{ id: string; date: string; adherentNom: string; montant: number }>>();
for (const p of participations) {
const prescripteur = p.adherent.prescripteur;
if (!prescripteur) continue;
const montant = p.montant ?? 0;
if (['envoye', 'paye'].includes(p.statut)) {
consomméMap.set(prescripteur, (consomméMap.get(prescripteur) ?? 0) + montant);
}
const hist = historiqueMap.get(prescripteur) ?? [];
hist.push({
id: p.id,
date: p.trajet.date.toISOString(),
adherentNom: `${p.adherent.prenom} ${p.adherent.nom}`,
montant,
});
historiqueMap.set(prescripteur, hist);
}
const budgetMap = new Map(budgets.map((b) => [b.prescripteurValue, { montant: b.montant, ajustementConsomme: b.ajustementConsomme ?? 0 }]));
const items = prescripteurs.map((p) => {
const budgetData = budgetMap.get(p.value);
const montant = budgetData?.montant ?? 0;
const ajustementConsomme = budgetData?.ajustementConsomme ?? 0;
const consomméCalculé = consomméMap.get(p.value) ?? 0;
const consommé = consomméCalculé + ajustementConsomme;
const restant = Math.max(0, montant - consommé);
const historique = (historiqueMap.get(p.value) ?? []).sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
return {
id: p.id,
value: p.value,
montant,
consommé,
restant,
ajustementConsomme,
historique,
};
});
const totalAlloue = items.reduce((s, i) => s + i.montant, 0);
const totalConsomme = items.reduce((s, i) => s + i.consommé, 0);
const totalRestant = Math.max(0, totalAlloue - totalConsomme);
return NextResponse.json({
global: { totalAlloue, totalConsomme, totalRestant },
prescripteurs: items,
});
} catch (error) {
console.error('Erreur lors de la récupération des budgets:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest) {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const body = await request.json();
const { prescripteurValue, montant: montantRaw, montantToAdd, rectifier, ajustementConsomme: ajRaw } = body;
if (!prescripteurValue || typeof prescripteurValue !== 'string') {
return NextResponse.json({ error: 'Prescripteur requis' }, { status: 400 });
}
const exists = await prisma.adherentOption.findFirst({
where: { type: 'prescripteur', value: prescripteurValue },
});
if (!exists) {
return NextResponse.json({ error: 'Prescripteur non trouvé' }, { status: 404 });
}
const existing = await prisma.prescripteurBudget.findUnique({
where: { prescripteurValue },
});
let montantFinal: number;
let ajustementFinal: number | undefined;
if (rectifier) {
const montant = typeof montantRaw === 'number' ? montantRaw : parseFloat(String(montantRaw ?? 0).replace(',', '.'));
if (isNaN(montant) || montant < 0) {
return NextResponse.json({ error: 'Budget invalide' }, { status: 400 });
}
montantFinal = montant;
const aj = typeof ajRaw === 'number' ? ajRaw : parseFloat(String(ajRaw ?? 0).replace(',', '.'));
ajustementFinal = isNaN(aj) ? 0 : aj;
} else if (montantToAdd !== undefined && montantToAdd !== null) {
const toAdd = typeof montantToAdd === 'number' ? montantToAdd : parseFloat(String(montantToAdd).replace(',', '.'));
if (isNaN(toAdd) || toAdd <= 0) {
return NextResponse.json({ error: 'Montant à ajouter invalide' }, { status: 400 });
}
montantFinal = (existing?.montant ?? 0) + toAdd;
} else {
const montant = typeof montantRaw === 'number' ? montantRaw : parseFloat(String(montantRaw ?? 0).replace(',', '.'));
if (isNaN(montant) || montant < 0) {
return NextResponse.json({ error: 'Montant invalide' }, { status: 400 });
}
montantFinal = montant;
}
const updateData: { montant: number; ajustementConsomme?: number } = { montant: montantFinal };
if (ajustementFinal !== undefined) {
updateData.ajustementConsomme = ajustementFinal;
}
const createData: { prescripteurValue: string; montant: number; ajustementConsomme?: number } = {
prescripteurValue,
montant: montantFinal,
};
if (ajustementFinal !== undefined) {
createData.ajustementConsomme = ajustementFinal;
}
const budget = await prisma.prescripteurBudget.upsert({
where: { prescripteurValue },
update: updateData,
create: createData,
});
return NextResponse.json(budget);
} catch (error) {
console.error('Erreur lors de la mise à jour du budget:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}

View File

@@ -18,8 +18,12 @@ export async function POST(
const participation = await prisma.participationFinanciere.findUnique({
where: { id: params.id },
include: {
adherent: { select: { prenom: true, nom: true } },
trajet: { select: { date: true } },
adherent: {
select: { prenom: true, nom: true, email: true, facturation: true, prescripteur: true },
},
trajet: {
select: { date: true, universProId: true, universPro: { select: { email: true, prenom: true, nom: true, nomEntreprise: true } } },
},
},
});
@@ -27,6 +31,34 @@ export async function POST(
return NextResponse.json({ error: 'Participation non trouvée' }, { status: 404 });
}
// Déterminer l'email selon la facturation de l'adhérent
const facturation = (participation.adherent.facturation || '').trim();
const isAdherent = !facturation || /^adh[eé]rent$/i.test(facturation);
let destinataireEmail: string;
let destinataireNom: string;
if (isAdherent) {
destinataireEmail = participation.adherent.email;
destinataireNom = `${participation.adherent.prenom} ${participation.adherent.nom}`;
} else if (participation.trajet?.universProId && participation.trajet?.universPro) {
destinataireEmail = participation.trajet.universPro.email;
destinataireNom = `${participation.trajet.universPro.prenom} ${participation.trajet.universPro.nom} - ${participation.trajet.universPro.nomEntreprise}`;
} else {
const universPro = await prisma.universPro.findFirst({
where: { nomEntreprise: facturation },
});
if (universPro) {
destinataireEmail = universPro.email;
destinataireNom = `${universPro.prenom} ${universPro.nom} - ${universPro.nomEntreprise}`;
} else {
return NextResponse.json(
{ error: `Aucune fiche Univers Pro trouvée pour "${facturation}". Créez un contact avec ce nom d'entreprise ou vérifiez la facturation de l'adhérent.` },
{ status: 400 }
);
}
}
const filePath = getParticipationStoragePath(participation.id);
if (!fs.existsSync(filePath)) {
return NextResponse.json(
@@ -72,7 +104,7 @@ export async function POST(
await transporter.sendMail({
from: process.env.SMTP_FROM || smtpUser,
to: participation.destinataireEmail,
to: destinataireEmail,
subject: `Participation financière - ${participation.adherent.prenom} ${participation.adherent.nom} - ${dateTrajet}`,
text: `Bonjour,\n\nVeuillez trouver ci-joint la participation financière concernant le trajet du ${dateTrajet} pour ${participation.adherent.prenom} ${participation.adherent.nom}.\n\nCordialement`,
html: `
@@ -88,14 +120,19 @@ export async function POST(
],
});
// Mettre à jour le statut en "envoyé"
// Mettre à jour le statut en "envoyé" et le destinataire (au cas où il aurait changé)
await prisma.participationFinanciere.update({
where: { id: params.id },
data: { statut: 'envoye' },
data: {
statut: 'envoye',
destinataireEmail,
destinataireNom,
destinataireType: isAdherent ? 'adherent' : 'univers_pro',
},
});
return NextResponse.json({
message: `Participation envoyée à ${participation.destinataireEmail}`,
message: `Participation envoyée à ${destinataireEmail}. Le budget du prescripteur a été décrementé.`,
});
} catch (error) {
console.error('Erreur lors de l\'envoi de l\'email:', error);