portail-associations/shared/sallePricing.ts

298 lines
8.8 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.

export type SalleUsageType = "conventionne" | "occasionnel";
export type SalleReservationFrequency = "demi_journee" | "journee" | "mensuel";
export type SalleWorkflowQuoteStatus = "a_preparer" | "en_attente_association" | "accepte" | "refuse";
export type SalleWorkflowDirectorStatus = "a_transmettre" | "en_attente_signature" | "signee";
export type SalleWorkflowPaymentStatus = "en_attente_paiement" | "paiement_partiel" | "paiement_recu" | "paiement_valide";
export type SallePricingLineItem = {
salleId: string;
salleNom: string;
categoryLabel: string;
usageType: SalleUsageType;
frequency: SalleReservationFrequency;
quantity: number;
unitAmountCents: number;
totalAmountCents: number;
};
export type SallePricingSummary = {
usageType: SalleUsageType;
frequency: SalleReservationFrequency;
quantity: number;
lineItems: SallePricingLineItem[];
totalAmountCents: number;
unsupportedSalles: Array<{
salleId: string;
salleNom: string;
}>;
};
export type SalleWorkflowData = {
usageType?: SalleUsageType;
frequency?: SalleReservationFrequency;
conditionsFinancieresText?: string;
decisionAdministrativeText?: string;
pricing?: SallePricingSummary;
quoteStatus?: SalleWorkflowQuoteStatus;
quoteSentAt?: string;
quoteRespondedAt?: string;
quotePdfUrl?: string;
quotePdfName?: string;
administrativeDecisionPdfUrl?: string;
administrativeDecisionPdfName?: string;
directorStatus?: SalleWorkflowDirectorStatus;
directorTransmissionAt?: string;
directorSignedAt?: string;
directorSignedByUserId?: number;
directorSignedByName?: string;
directorReturnComment?: string;
directorReturnedAt?: string;
signedQuotePdfUrl?: string;
signedQuotePdfName?: string;
signedDecisionPdfUrl?: string;
signedDecisionPdfName?: string;
signedRequestPdfUrl?: string;
signedRequestPdfName?: string;
invoicePdfUrl?: string;
invoicePdfName?: string;
invoiceGeneratedAt?: string;
paymentStatus?: SalleWorkflowPaymentStatus;
paymentDueDate?: string;
amountReceivedCents?: number;
paymentReceivedAt?: string;
paymentValidatedAt?: string;
paymentReference?: string;
paymentNotes?: string;
finalNotificationSentAt?: string;
};
type SalleTariffDefinition = {
salleId: string;
salleNom: string;
categoryLabel: string;
pricing: Partial<Record<SalleUsageType, Partial<Record<SalleReservationFrequency, number>>>>;
};
export const SALLE_USAGE_TYPE_LABELS: Record<SalleUsageType, string> = {
conventionne: "Conventionné",
occasionnel: "Occasionnel",
};
export const SALLE_FREQUENCY_LABELS: Record<SalleReservationFrequency, string> = {
demi_journee: "Demi-journée",
journee: "Journée",
mensuel: "Mensuel",
};
export const SALLE_WORKFLOW_QUOTE_STATUS_LABELS: Record<SalleWorkflowQuoteStatus, string> = {
a_preparer: "À préparer",
en_attente_association: "En attente de l'association",
accepte: "Accepté par l'association",
refuse: "Refusé par l'association",
};
export const SALLE_WORKFLOW_DIRECTOR_STATUS_LABELS: Record<SalleWorkflowDirectorStatus, string> = {
a_transmettre: "À transmettre à la Directrice",
en_attente_signature: "En attente de signature",
signee: "Signée",
};
export const SALLE_WORKFLOW_PAYMENT_STATUS_LABELS: Record<SalleWorkflowPaymentStatus, string> = {
en_attente_paiement: "En attente de paiement",
paiement_partiel: "Paiement partiel",
paiement_recu: "Paiement reçu",
paiement_valide: "Paiement validé",
};
const SALLE_TARIFFS: SalleTariffDefinition[] = [
{
salleId: "bureau_11_17",
salleNom: "Bureau 1117 m²",
categoryLabel: "Bureau 1117 m²",
pricing: {
conventionne: {
journee: 1216,
mensuel: 18819,
},
},
},
{
salleId: "salle_toucan",
salleNom: "Salle TOUCAN",
categoryLabel: "Salle de conférence - Formation",
pricing: {
conventionne: {
demi_journee: 7673,
journee: 15345,
},
},
},
{
salleId: "salle_ibis",
salleNom: "Salle IBIS",
categoryLabel: "Espace formation",
pricing: {
conventionne: {
demi_journee: 8173,
journee: 15845,
mensuel: 25000,
},
},
},
{
salleId: "salle_pelican",
salleNom: "Salle PELICAN",
categoryLabel: "Espace formation",
pricing: {
conventionne: {
demi_journee: 8173,
journee: 15845,
mensuel: 25000,
},
},
},
{
salleId: "hall_amphitheatre",
salleNom: "Hall-Amphithéâtre",
categoryLabel: "Hall ouvert - 343,34 m²",
pricing: {
conventionne: {
demi_journee: 5000,
journee: 10000,
},
},
},
{
salleId: "dojo",
salleNom: "DOJO",
categoryLabel: "Dojo",
pricing: {
conventionne: {
mensuel: 65000,
},
occasionnel: {
mensuel: 25000,
},
},
},
];
export const CONDITIONS_FINANCIERES_SALLE_TEXT = `CONDITIONS FINANCIERES - LOCATION DES ESPACES
La mise a disposition d'un espace au sein de la Maison de la Jeunesse des Savanes « Melissa ALVES » est soumise aux conditions suivantes :
1. Tarification
Les tarifs appliques sont ceux votes par le Conseil Communautaire, actualises au 1er septembre 2025.
Le montant est calcule automatiquement en fonction :
de l'espace choisi
de la duree (journee, demi-journee, mensuel)
du type d'utilisateur (conventionne / occasionnel)
2. Acompte
Un acompte peut etre demande selon la nature de l'evenement.
3. Annulation
Toute annulation doit etre signalee 48 heures a l'avance pour un eventuel remboursement de l'acompte.
4. Responsabilite
L'association est responsable :
des participants
du public
des dommages eventuels
du respect des regles de securite
Une attestation de responsabilite civile couvrant la periode doit etre fournie.
5. Entretien des locaux
Il est interdit de manger dans les salles.
Les espaces doivent etre rendus propres et en bon etat.`;
export function buildDecisionAdministrativeText(input: {
espace: string;
dates: string;
horaires: string;
association: string;
}) {
return `DECISION ADMINISTRATIVE PRIORITAIRE
La presente decision atteste que la Maison de la Jeunesse des Savanes « Melissa ALVES » donne un avis favorable a la mise a disposition de l'espace suivant :
Espace : ${input.espace}
Dates : ${input.dates}
Horaires : ${input.horaires}
Association : ${input.association}
Cette decision est transmise avant validation finale et ne vaut pas autorisation definitive.
La mise a disposition devient effective uniquement apres :
validation du devis (si facturation)
signature de la Directrice
reception de l'attestation de responsabilite civile`;
}
export function getSalleTariffDefinition(salleId: string) {
return SALLE_TARIFFS.find((entry) => entry.salleId === salleId) || null;
}
export function listReservationDates(start: string, end: string) {
if (!start || !end) return [] as string[];
const startDate = new Date(`${start}T12:00:00`);
const endDate = new Date(`${end}T12:00:00`);
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()) || startDate > endDate) {
return [] as string[];
}
const values: string[] = [];
const cursor = new Date(startDate);
while (cursor <= endDate) {
values.push(cursor.toISOString().slice(0, 10));
cursor.setDate(cursor.getDate() + 1);
}
return values;
}
export function getReservationUnitQuantity(input: {
frequency: SalleReservationFrequency;
dateReservation?: string;
dateFinReservation?: string;
}) {
if (input.frequency === "mensuel") return 1;
const dates = listReservationDates(input.dateReservation || "", input.dateFinReservation || input.dateReservation || "");
return Math.max(dates.length, 1);
}
export function computeSallePricing(input: {
sallesIds: string[];
usageType: SalleUsageType;
frequency: SalleReservationFrequency;
dateReservation?: string;
dateFinReservation?: string;
}) {
const quantity = getReservationUnitQuantity(input);
const summary: SallePricingSummary = {
usageType: input.usageType,
frequency: input.frequency,
quantity,
lineItems: [],
totalAmountCents: 0,
unsupportedSalles: [],
};
input.sallesIds.forEach((salleId) => {
const salle = getSalleTariffDefinition(salleId);
if (!salle) {
summary.unsupportedSalles.push({ salleId, salleNom: salleId });
return;
}
const unitAmountCents = salle.pricing[input.usageType]?.[input.frequency];
if (typeof unitAmountCents !== "number") {
summary.unsupportedSalles.push({ salleId, salleNom: salle.salleNom });
return;
}
const lineItem: SallePricingLineItem = {
salleId,
salleNom: salle.salleNom,
categoryLabel: salle.categoryLabel,
usageType: input.usageType,
frequency: input.frequency,
quantity,
unitAmountCents,
totalAmountCents: unitAmountCents * quantity,
};
summary.lineItems.push(lineItem);
summary.totalAmountCents += lineItem.totalAmountCents;
});
return summary;
}