xtablo-source/ui/src/utils/helpers.ts
Arthur Belleville b3a7ced5c7
Lint
2025-10-10 11:30:13 +02:00

348 lines
11 KiB
TypeScript

import { Database } from "@ui/types/database.types";
import { EventAndTablo } from "@ui/types/events.types";
import jsPDF from "jspdf";
export const calculateTax = (amount: number, taxRate: number) => {
return (amount * taxRate) / 100;
};
export const calculateTotal = (amount: number, tax: number) => {
return amount + tax;
};
export const statusToText: Record<Devis["status"], string> = {
draft: "Brouillon",
sent: "Envoyé",
accepted: "Accepté",
rejected: "Rejeté",
expired: "Expiré",
};
type Devis = Database["public"]["Tables"]["devis"]["Row"];
export const exportDevisToPdf = (devis: Devis) => {
const doc = new jsPDF();
// --- Basic PDF Content ---
doc.setFontSize(22);
doc.text(`Devis ${devis.number}`, 20, 20);
doc.setFontSize(12);
doc.text(`Client: ${devis.client_email}`, 20, 40);
doc.text(`Date: ${new Date(devis.date).toLocaleDateString("fr-FR")}`, 140, 40);
doc.text(`Date d'échéance: ${new Date(devis.due_date).toLocaleDateString("fr-FR")}`, 140, 48);
// --- Amounts ---
const startYAmounts = 70;
doc.line(20, startYAmounts - 5, 190, startYAmounts - 5); // Separator line
doc.text(`Sous-total HT: ${devis.subtotal.toFixed(2)}`, 140, startYAmounts);
doc.text(`TVA: ${devis.tax.toFixed(2)}`, 140, startYAmounts + 8);
doc.setFontSize(14);
doc.setFont("helvetica", "bold");
doc.text(`Total TTC: ${devis.total.toFixed(2)}`, 140, startYAmounts + 18);
doc.setFont("helvetica", "normal"); // Reset font
doc.line(20, startYAmounts + 25, 190, startYAmounts + 25); // Separator line
// --- Notes & Terms (if available) ---
let currentY = startYAmounts + 40;
if (devis.notes) {
doc.setFontSize(12);
doc.setFont("helvetica", "bold");
doc.text("Notes:", 20, currentY);
doc.setFont("helvetica", "normal");
// Use splitTextToSize for wrapping long text
const notesLines = doc.splitTextToSize(devis.notes, 170); // 170 is max width
doc.text(notesLines, 20, currentY + 8);
currentY += notesLines.length * 5 + 15; // Adjust Y position based on lines
}
if (devis.terms) {
doc.setFontSize(12);
doc.setFont("helvetica", "bold");
doc.text("Conditions:", 20, currentY);
doc.setFont("helvetica", "normal");
const termsLines = doc.splitTextToSize(devis.terms, 170);
doc.text(termsLines, 20, currentY + 8);
}
// --- Save the PDF ---
doc.save(`devis_${devis.number}.pdf`);
};
export const isStaging = import.meta.env.MODE === "staging";
export const isProd = import.meta.env.MODE === "production";
// ICS Export functionality
export const generateICSFromEvents = (
events: EventAndTablo[],
calendarName: string = "Planning"
) => {
const formatDate = (date: string, time: string) => {
// Combine date (YYYY-MM-DD) and time (HH:MM:SS) into ISO format then convert to UTC
const dateTime = new Date(`${date}T${time}`);
return `${dateTime.toISOString().replace(/[-:]/g, "").split(".")[0]}Z`;
};
const escapeICSText = (text: string) => {
return text
.replace(/\\/g, "\\\\")
.replace(/;/g, "\\;")
.replace(/,/g, "\\,")
.replace(/\n/g, "\\n")
.replace(/\r/g, "");
};
const generateUID = (eventId: string) => {
return `${eventId}@xtablo.com`;
};
let icsContent = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//XTablo//Planning Export//EN",
`X-WR-CALNAME:${escapeICSText(calendarName)}`,
"X-WR-TIMEZONE:Europe/Paris",
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
].join("\r\n");
events.forEach((event) => {
if (!event.start_date || !event.start_time || !event.title) return;
const startDateTime = formatDate(event.start_date, event.start_time);
const endDateTime = event.end_time
? formatDate(event.start_date, event.end_time)
: formatDate(event.start_date, event.start_time); // Default to start time if no end time
const eventLines = [
"",
"BEGIN:VEVENT",
`UID:${generateUID(event.event_id)}`,
`DTSTART:${startDateTime}`,
`DTEND:${endDateTime}`,
`SUMMARY:${escapeICSText(event.title)}`,
`DESCRIPTION:${escapeICSText(`Tablo: ${event.tablo_name}\n${event.description || ""}`)}`,
event.tablo_name ? `CATEGORIES:${escapeICSText(event.tablo_name)}` : "",
`CREATED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`,
`LAST-MODIFIED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`,
"STATUS:CONFIRMED",
"TRANSP:OPAQUE",
"END:VEVENT",
].filter((line) => line !== ""); // Remove empty lines
icsContent += `\r\n${eventLines.join("\r\n")}`;
});
icsContent += "\r\n" + "END:VCALENDAR";
return icsContent;
};
export const downloadICSFile = (icsContent: string, filename: string = "planning.ics") => {
const blob = new Blob([icsContent], { type: "text/calendar;charset=utf-8" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
};
// ICS Import functionality
export interface ParsedICSEvent {
title: string;
description?: string;
start_date: string;
start_time: string;
end_time?: string;
}
export const parseICSFile = (icsContent: string): ParsedICSEvent[] => {
const events: ParsedICSEvent[] = [];
const lines = icsContent.split(/\r?\n/);
let currentEvent: Partial<ParsedICSEvent> | null = null;
const unescapeICSText = (text: string) => {
return text
.replace(/\\n/g, "\n")
.replace(/\\,/g, ",")
.replace(/\\;/g, ";")
.replace(/\\\\/g, "\\");
};
const parseDateTime = (dateTimeStr: string) => {
// Handle both YYYYMMDDTHHMMSSZ and YYYYMMDDTHHMMSS formats
const cleanDate = dateTimeStr.replace(/[TZ]/g, "");
if (cleanDate.length >= 8) {
const year = cleanDate.substring(0, 4);
const month = cleanDate.substring(4, 6);
const day = cleanDate.substring(6, 8);
const date = `${year}-${month}-${day}`;
if (cleanDate.length >= 14) {
const hour = cleanDate.substring(8, 10);
const minute = cleanDate.substring(10, 12);
const second = cleanDate.substring(12, 14) || "00";
const time = `${hour}:${minute}:${second}`;
return { date, time };
} else {
return { date, time: "00:00:00" };
}
}
return null;
};
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
// Handle line folding (lines starting with space or tab)
while (i + 1 < lines.length && /^[ \t]/.test(lines[i + 1])) {
line += lines[i + 1].substring(1);
i++;
}
if (line === "BEGIN:VEVENT") {
currentEvent = {};
} else if (line === "END:VEVENT" && currentEvent) {
if (currentEvent.title && currentEvent.start_date && currentEvent.start_time) {
events.push(currentEvent as ParsedICSEvent);
}
currentEvent = null;
} else if (currentEvent && line.includes(":")) {
const colonIndex = line.indexOf(":");
const property = line.substring(0, colonIndex);
const value = line.substring(colonIndex + 1);
// Handle properties that might have parameters (e.g., DTSTART;TZID=...)
const [propName] = property.split(";");
switch (propName) {
case "SUMMARY":
currentEvent.title = unescapeICSText(value);
break;
case "DESCRIPTION":
currentEvent.description = unescapeICSText(value);
break;
case "DTSTART": {
const startDateTime = parseDateTime(value);
if (startDateTime) {
currentEvent.start_date = startDateTime.date;
currentEvent.start_time = startDateTime.time;
}
break;
}
case "DTEND": {
const endDateTime = parseDateTime(value);
if (endDateTime) {
// Only use end time if it's on the same date
const startDate = currentEvent.start_date;
if (startDate === endDateTime.date) {
currentEvent.end_time = endDateTime.time;
}
}
break;
}
}
}
}
return events;
};
export const getTextColorFromTabloColor = (tabloColor: string): string => {
// Map of tablo background colors to their corresponding text colors
const colorMap: Record<string, string> = {
// Blue variants
"bg-blue-100": "text-blue-800",
"bg-blue-200": "text-blue-900",
"bg-blue-300": "text-blue-900",
"bg-blue-400": "text-white",
"bg-blue-500": "text-white",
"bg-blue-600": "text-white",
"bg-blue-700": "text-white",
"bg-blue-800": "text-white",
"bg-blue-900": "text-white",
// Green variants
"bg-green-100": "text-green-800",
"bg-green-200": "text-green-900",
"bg-green-300": "text-green-900",
"bg-green-400": "text-white",
"bg-green-500": "text-white",
"bg-green-600": "text-white",
"bg-green-700": "text-white",
"bg-green-800": "text-white",
"bg-green-900": "text-white",
// Red variants
"bg-red-100": "text-red-800",
"bg-red-200": "text-red-900",
"bg-red-300": "text-red-900",
"bg-red-400": "text-white",
"bg-red-500": "text-white",
"bg-red-600": "text-white",
"bg-red-700": "text-white",
"bg-red-800": "text-white",
"bg-red-900": "text-white",
// Yellow variants
"bg-yellow-100": "text-yellow-800",
"bg-yellow-200": "text-yellow-900",
"bg-yellow-300": "text-yellow-900",
"bg-yellow-400": "text-yellow-900",
"bg-yellow-500": "text-white",
"bg-yellow-600": "text-white",
"bg-yellow-700": "text-white",
"bg-yellow-800": "text-white",
"bg-yellow-900": "text-white",
// Purple variants
"bg-purple-100": "text-purple-800",
"bg-purple-200": "text-purple-900",
"bg-purple-300": "text-purple-900",
"bg-purple-400": "text-white",
"bg-purple-500": "text-white",
"bg-purple-600": "text-white",
"bg-purple-700": "text-white",
"bg-purple-800": "text-white",
"bg-purple-900": "text-white",
// Pink variants
"bg-pink-100": "text-pink-800",
"bg-pink-200": "text-pink-900",
"bg-pink-300": "text-pink-900",
"bg-pink-400": "text-white",
"bg-pink-500": "text-white",
"bg-pink-600": "text-white",
"bg-pink-700": "text-white",
"bg-pink-800": "text-white",
"bg-pink-900": "text-white",
// Indigo variants
"bg-indigo-100": "text-indigo-800",
"bg-indigo-200": "text-indigo-900",
"bg-indigo-300": "text-indigo-900",
"bg-indigo-400": "text-white",
"bg-indigo-500": "text-white",
"bg-indigo-600": "text-white",
"bg-indigo-700": "text-white",
"bg-indigo-800": "text-white",
"bg-indigo-900": "text-white",
// Gray variants
"bg-gray-100": "text-gray-800",
"bg-gray-200": "text-gray-900",
"bg-gray-300": "text-gray-900",
"bg-gray-400": "text-white",
"bg-gray-500": "text-white",
"bg-gray-600": "text-white",
"bg-gray-700": "text-white",
"bg-gray-800": "text-white",
"bg-gray-900": "text-white",
};
return colorMap[tabloColor] || "text-gray-900";
};