Add ICS export functionality to Planning
This commit is contained in:
parent
a08375ad42
commit
394fc3fd22
2 changed files with 128 additions and 0 deletions
|
|
@ -9,6 +9,7 @@ import {
|
|||
SelectListItem,
|
||||
} from "@ui/ui-library/select";
|
||||
import { Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
import { generateICSFromEvents, downloadICSFile } from "@ui/utils/helpers";
|
||||
|
||||
type ViewType = "month" | "week" | "day";
|
||||
|
||||
|
|
@ -31,6 +32,27 @@ export const PlanningPage = () => {
|
|||
|
||||
const deleteEvent = useDeleteEvent();
|
||||
|
||||
const handleExportICS = () => {
|
||||
if (!tabloEvents || tabloEvents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const calendarName =
|
||||
selectedTabloId === "all"
|
||||
? "Planning - Tous les tablos"
|
||||
: tablos?.find((t) => t.id === selectedTabloId)?.name || "Planning";
|
||||
|
||||
const icsContent = generateICSFromEvents(tabloEvents, calendarName);
|
||||
const filename =
|
||||
selectedTabloId === "all"
|
||||
? "planning-tous-tablos.ics"
|
||||
: `planning-${
|
||||
tablos?.find((t) => t.id === selectedTabloId)?.name || "tablo"
|
||||
}.ics`;
|
||||
|
||||
downloadICSFile(icsContent, filename);
|
||||
};
|
||||
|
||||
const navigateToCreateEvent = (date: Date, tablo_id: string) => {
|
||||
if (tablo_id === "all") {
|
||||
navigate({
|
||||
|
|
@ -732,6 +754,27 @@ export const PlanningPage = () => {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleExportICS}
|
||||
disabled={!tabloEvents || tabloEvents.length === 0}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
|
||||
title="Exporter en format ICS"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Exporter</span>
|
||||
</button>
|
||||
<div className="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
|
||||
{(["month", "week", "day"] as ViewType[]).map((view) => (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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) => {
|
||||
|
|
@ -79,3 +80,87 @@ export const exportDevisToPdf = (devis: Devis) => {
|
|||
|
||||
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;
|
||||
|
||||
console.log("event", event);
|
||||
|
||||
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(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"
|
||||
) => {
|
||||
console.log("icsContent", icsContent);
|
||||
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);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue