✨ Trip: export to ics or csv, sort order
This commit is contained in:
parent
a30e7eaec4
commit
56b1d008f0
@ -1,93 +1,41 @@
|
|||||||
import { FlattenedTripItem } from '../../types/trip';
|
import { FlattenedTripItem } from '../../types/trip';
|
||||||
import { UtilsService } from '../../services/utils.service';
|
|
||||||
|
|
||||||
export function generateTripICSFile(
|
export function generateTripCSVFile(tripItems: readonly FlattenedTripItem[], tripName: string = 'Trip Calendar'): void {
|
||||||
tripItems: FlattenedTripItem[],
|
const headers = ['date', 'label', 'time', 'text', 'place', 'comment', 'latlng', 'price', 'status'];
|
||||||
tripName: string = 'Trip Calendar',
|
|
||||||
utilsService: UtilsService,
|
|
||||||
): void {
|
|
||||||
const now = new Date();
|
|
||||||
const tsz = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
|
||||||
|
|
||||||
let icsContent = [
|
let csvContent = headers.join(',') + '\n';
|
||||||
'BEGIN:VCALENDAR',
|
|
||||||
'VERSION:2.0',
|
|
||||||
'PRODID:-//Trip//Trip Calendar//EN',
|
|
||||||
'CALSCALE:GREGORIAN',
|
|
||||||
'METHOD:PUBLISH',
|
|
||||||
`X-WR-CALNAME:${tripName}`,
|
|
||||||
'X-WR-TIMEZONE:Europe/Paris',
|
|
||||||
].join('\r\n');
|
|
||||||
|
|
||||||
if (tripItems.some((i) => !i.td_date))
|
tripItems.forEach((item) => {
|
||||||
utilsService.toast('warn', 'Caution', 'You have date-less days, they will not be included in your export');
|
const row = [
|
||||||
|
item.td_date ?? '',
|
||||||
tripItems.forEach((item, index) => {
|
item.td_label,
|
||||||
if (!item.td_date) return;
|
item.time ?? '',
|
||||||
|
escape_rfc4180(item.text),
|
||||||
const eventDate = item.td_date;
|
escape_rfc4180(item.comment ?? ''),
|
||||||
const eventTime = item.time ?? '00:00';
|
escape_rfc4180(item.place?.name ?? ''),
|
||||||
const [year, month, day] = eventDate.split('-');
|
item.lat ?? item.place?.lat ?? '',
|
||||||
const [hours, minutes] = eventTime.split(':');
|
item.lng ?? item.place?.lng ?? '',
|
||||||
const dtStart = `${year}${month}${day}T${hours.padStart(2, '0')}${minutes.padStart(2, '0')}00`;
|
item.price ?? '',
|
||||||
|
item.status?.label ?? '',
|
||||||
const startDateTime = new Date(`${eventDate}T${eventTime}`);
|
];
|
||||||
const nextItemSameDay = tripItems.slice(index + 1).find((i) => i.td_date === item.td_date && i.time);
|
csvContent += row.join(',') + '\n';
|
||||||
|
|
||||||
const endDateTime = nextItemSameDay?.time
|
|
||||||
? new Date(`${nextItemSameDay.td_date}T${nextItemSameDay.time}`)
|
|
||||||
: new Date(startDateTime.getTime() + 60 * 60 * 1000);
|
|
||||||
const dtEnd = endDateTime.toISOString().replace(/[-:]/g, '').split('.')[0];
|
|
||||||
|
|
||||||
const eventDescription: string[] = [];
|
|
||||||
if (item.comment) eventDescription.push(`Comment: ${item.comment}`);
|
|
||||||
if (item.place?.name) eventDescription.push(`Place: ${item.place.name}`);
|
|
||||||
|
|
||||||
const lat = item.lat ?? item.place?.lat;
|
|
||||||
const lng = item.lng ?? item.place?.lng;
|
|
||||||
if (lat && lng) {
|
|
||||||
eventDescription.push(`Coordinates: ${lat}, ${lng}`);
|
|
||||||
eventDescription.push(`GMaps: https://www.google.com/maps?q=${lat},${lng}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.price) eventDescription.push(`Price: ${item.price}€`);
|
|
||||||
|
|
||||||
const description = eventDescription.join('\\n').replace(/\n/g, '\\n');
|
|
||||||
const location = item.place?.name ?? (lat && lng ? `${lat}, ${lng}` : '');
|
|
||||||
const geo = lat && lng ? `GEO:${lat};${lng}` : '';
|
|
||||||
const uid = `Trip-${tripName.replace(/[^a-zA-Z0-9-_]/g, '_')}-item-${item.id}-${tsz}`;
|
|
||||||
|
|
||||||
icsContent +=
|
|
||||||
'\r\n' +
|
|
||||||
[
|
|
||||||
'BEGIN:VEVENT',
|
|
||||||
`UID:${uid}`,
|
|
||||||
`DTSTAMP:${tsz}`,
|
|
||||||
`DTSTART:${dtStart}`,
|
|
||||||
`DTEND:${dtEnd}`,
|
|
||||||
`SUMMARY:${escapeICSText(item.text)}`,
|
|
||||||
description ? `DESCRIPTION:${escapeICSText(description)}` : '',
|
|
||||||
location ? `LOCATION:${escapeICSText(location)}` : '',
|
|
||||||
geo,
|
|
||||||
item.status ? `STATUS:${item.status.label.toUpperCase()}` : '',
|
|
||||||
'END:VEVENT',
|
|
||||||
]
|
|
||||||
.filter((line) => line)
|
|
||||||
.join('\r\n');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
icsContent += '\r\n' + 'END:VCALENDAR';
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
const blob = new Blob([icsContent], { type: 'text/calendar;charset=utf-8' });
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = URL.createObjectURL(blob);
|
link.href = url;
|
||||||
link.download = `${tripName.replace(/\s+/g, '_')}.ics`;
|
link.download = `${tripName.replace(/\s+/g, '_')}.csv`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(link.href);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeICSText(text: string): string {
|
function escape_rfc4180(field: string): string {
|
||||||
if (!text) return '';
|
if (!field) return '';
|
||||||
return text.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n').replace(/\r/g, '');
|
if (field.includes(',') || field.includes('"') || field.includes('\n')) {
|
||||||
|
return `"${field.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return field;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,6 @@ export function generateTripICSFile(
|
|||||||
const dtStart = `${year}${month}${day}T${hours.padStart(2, '0')}${minutes.padStart(2, '0')}00`;
|
const dtStart = `${year}${month}${day}T${hours.padStart(2, '0')}${minutes.padStart(2, '0')}00`;
|
||||||
|
|
||||||
const startDateTime = new Date(`${eventDate}T${eventTime}`);
|
const startDateTime = new Date(`${eventDate}T${eventTime}`);
|
||||||
|
|
||||||
const nextItem = tripItems.slice(index + 1).find((i) => i.td_date);
|
|
||||||
const nextItemSameDay = tripItems.slice(index + 1).find((i) => i.td_date === item.td_date && i.time);
|
const nextItemSameDay = tripItems.slice(index + 1).find((i) => i.td_date === item.td_date && i.time);
|
||||||
|
|
||||||
const endDateTime = nextItemSameDay?.time
|
const endDateTime = nextItemSameDay?.time
|
||||||
@ -41,11 +39,8 @@ export function generateTripICSFile(
|
|||||||
: new Date(startDateTime.getTime() + 60 * 60 * 1000);
|
: new Date(startDateTime.getTime() + 60 * 60 * 1000);
|
||||||
const dtEnd = endDateTime.toISOString().replace(/[-:]/g, '').split('.')[0];
|
const dtEnd = endDateTime.toISOString().replace(/[-:]/g, '').split('.')[0];
|
||||||
|
|
||||||
// Build description
|
|
||||||
const eventDescription: string[] = [];
|
const eventDescription: string[] = [];
|
||||||
|
|
||||||
if (item.comment) eventDescription.push(`Comment: ${item.comment}`);
|
if (item.comment) eventDescription.push(`Comment: ${item.comment}`);
|
||||||
|
|
||||||
if (item.place?.name) eventDescription.push(`Place: ${item.place.name}`);
|
if (item.place?.name) eventDescription.push(`Place: ${item.place.name}`);
|
||||||
|
|
||||||
const lat = item.lat ?? item.place?.lat;
|
const lat = item.lat ?? item.place?.lat;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user