/** */ type type_role = ( "editor" | "viewer" ); /** */ type type_user_id = int; /** */ type type_user_object = { name : string; }; /** */ type type_event = { name : string; begin : type_datetime; end : ( null | type_datetime ); description : ( null | string ); }; /** */ type type_calendar_id = int; /** * @todo bei "collection" Kreise vermeiden */ type type_calendar_object = ( { kind : "concrete"; data : { name : string; users : Array< { id : type_user_id; role : type_role; } >; events : Array; }; } | { kind : "collection"; data : { name : string; sources : Array< type_calendar_id >; } } | { kind : "caldav"; data : { source_url : string; } } ); /** */ type type_datamodel = { users : Array< { id : type_user_id; object : type_user_object; } >; calendars : Array< { id : type_calendar_id; object : type_calendar_object; } >; }; /** */ function calendar_list( data : type_datamodel ) : Array< { key : type_calendar_id; preview : { name : string; } } > { return ( data.calendars .map( (calendar_entry) => { switch (calendar_entry.object.kind) { case "concrete": { return { "key": calendar_entry.id, "preview": { "name": calendar_entry.object.data.name, } }; break; } case "collection": { return { "key": calendar_entry.id, "preview": { "name": calendar_entry.object.data.name, } }; } case "caldav": { return { "key": calendar_entry.id, "preview": { "name": "(imported)", } }; } } } ) ); } /** */ function calendar_read( data : type_datamodel, calendar_id : type_calendar_id ) : type_calendar_object { const hits = ( data.calendars .filter( (calendar_entry) => (calendar_entry.id === calendar_id) ) ); if (hits.length <= 0) { throw (new Error("not found")); } else { return hits[0].object; } } /** */ async function calendar_gather_events( data : type_datamodel, calendar_id : type_calendar_id, from_pit : type_pit, to_pit : type_pit ) : Promise< Array< { calendar_id : type_calendar_id; event : type_event; } > > { const calendar_object : type_calendar_object = calendar_read( data, calendar_id ); switch (calendar_object.kind) { case "concrete": { return Promise.resolve( calendar_object.data.events .filter( (event) => pit_is_between( pit_from_datetime(event.begin), from_pit, to_pit ) ) .map( (event) => ({"calendar_id": calendar_id, "event": event}) ) ); break; } case "collection": { return ( Promise.all( calendar_object.data.sources .map( (source_calendar_id) => calendar_gather_events( data, source_calendar_id, from_pit, to_pit ) ) ) .then( (sources) => Promise.resolve( sources .reduce( (x, y) => x.concat(y), [] ) ) ) ) break; } case "caldav": { const url : lib_plankton.url.type_url = lib_plankton.url.decode(calendar_object.data.source_url); const http_request : lib_plankton.http.type_request = { "version": "HTTP/1.1", "scheme": ((url.scheme === "https") ? "https" : "http"), "host": url.host, "path": (url.path ?? "/"), "query": url.query, "method": lib_plankton.http.enum_method.get, "headers": {}, "body": null, }; // TODO: cache? const http_response : lib_plankton.http.type_response = await lib_plankton.http.call( http_request, { } ); const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode( http_response.body.toString(), { } ); return Promise.resolve( vcalendar.vevents .map( (vevent : lib_plankton.ical.type_vevent) => ( (vevent.dtstart !== undefined) ? { "name": ( (vevent.summary !== undefined) ? vevent.summary : "???" ), "begin": ical_dt_to_own_datetime(vevent.dtstart), "end": ( (vevent.dtend !== undefined) ? ical_dt_to_own_datetime(vevent.dtend) : null ), "description": ( (vevent.description !== undefined) ? vevent.description : null ), } : null ) ) .filter( (event) => (event !== null) ) .filter( (event) => pit_is_between( pit_from_datetime(event.begin), from_pit, to_pit ) ) .map( (event) => ({"calendar_id": calendar_id, "event": event}) ) ); break; } } } /** * @todo kein "while" */ async function calendar_view_table_data( data : type_datamodel, calendar_id : type_calendar_id, options : { from ?: { year : int; week : int; }, to ?: { year : int; week : int; }, timezone_shift ?: int; } = {} ) : Promise< Array< { week : int; data : Array< { pit : type_pit; entries : Array< { calendar_id : type_calendar_id; event : type_event; } >; today : boolean; } >; } > > { const now_pit : type_pit = pit_now(); options = Object.assign( { "from": lib_plankton.call.convey( now_pit, [ (x : type_pit) => pit_shift_week(x, -1), pit_to_date_object, (x : Date) => ({ "year": x.getFullYear(), "week": date_object_get_week_of_year(x), }) ] ), "to": lib_plankton.call.convey( now_pit, [ (x : type_pit) => pit_shift_week(x, +4), pit_to_date_object, (x : Date) => ({ "year": x.getFullYear(), "week": date_object_get_week_of_year(x), }) ] ), "timezone_shift": 0, }, options ); /* const calendar_object : type_calendar_object = calendar_read( data, calendar_id ); */ const from_pit : type_pit = pit_from_year_and_week( (options.from as {year : int; week : int}).year, (options.from as {year : int; week : int}).week, { "timezone_shift": (options.timezone_shift as int), } ); const to_pit : type_pit = pit_from_year_and_week( (options.to as {year : int; week : int}).year, (options.to as {year : int; week : int}).week, { "timezone_shift": (options.timezone_shift as int), } ); // prepare const entries : Array< { calendar_id : type_calendar_id; event : type_event; } > = await calendar_gather_events( data, calendar_id, from_pit, to_pit ); let result : Array< { week : int; data : Array< { pit : type_pit; entries : Array< { calendar_id : type_calendar_id; event : type_event; } >; today : boolean; } >; } > = []; let row : Array< { pit : type_pit; entries : Array< { calendar_id : type_calendar_id; event : type_event; } >; today : boolean; } > = []; let day : int = 0; while (true) { const pit_current : type_pit = pit_shift_day(from_pit, day); if (pit_is_before(pit_current, to_pit)) { day += 1; row.push( { "pit": pit_current, "entries": [], "today": false, // TODO } ); if (day % 7 === 0) { result.push( { "week": ( (options.from as {year : int; week : int}).week + Math.floor(day / 7) - 1 // TODO ), "data": row } ); row = []; } else { // do nothing } } else { break; } } // fill { // events ( entries .forEach( (entry) => { const distance_seconds : int = (pit_from_datetime(entry.event.begin) - from_pit); const distance_days : int = (distance_seconds / (60 * 60 * 24)); const week : int = Math.floor(Math.floor(distance_days) / 7); const day : int = (Math.floor(distance_days) % 7); if ((week >= 0) && (week < result.length)) { result[week].data[day].entries.push(entry); } else { // do nothing } } ) ); // today { const distance_seconds : int = (now_pit - from_pit); const distance_days : int = (distance_seconds / (60 * 60 * 24)); const week : int = Math.floor(Math.floor(distance_days) / 7); const day : int = (Math.floor(distance_days) % 7); if ((week >= 0) && (week < result.length)) { result[week].data[day].today = true; } else { // do nothing } } } return Promise.resolve(result); } /** */ async function calendar_view_table_html( data : type_datamodel, calendar_id : type_calendar_id, options : { from ?: { year : int; week : int; }; to ?: { year : int; week : int; }; timezone_shift ?: int; } = {} ) : Promise { const stuff : Array< { week : int; data : Array< { pit : type_pit; entries : Array< { calendar_id : type_calendar_id; event : type_event; } >; today : boolean; } >; } > = await calendar_view_table_data( data, calendar_id, options ); return Promise.resolve( new lib_plankton.xml.class_node_complex( "div", { "class": "calendar", }, [ new lib_plankton.xml.class_node_complex( "style", {}, [ new lib_plankton.xml.class_node_text( "html {background-color: #111; color: #FFF; font-family: sans-serif;}\n" + "table {width: 100%; border-collapse: collapse;}\n" + ".calendar-cell {border: 1px solid #444; padding: 8px; vertical-align: top;}\n" + ".calendar-cell-day {width: 13.5%;}\n" + ".calendar-cell-week {width: 5.5%;}\n" + ".calendar-cell-regular {width: 13.5%; height: 150px;}\n" + ".calendar-cell-today {background-color: #333;}\n" + ".calendar-day {font-size: 0.75em;}\n" + ".calendar-events {margin: 0; padding: 0; list-style-type: none;}\n" + ".calendar-event_entry {margin: 4px; padding: 4px; border-radius: 2px; font-size: 0.75em; color: #FFF; font-weight: bold; cursor: pointer;}\n" ) ] ), new lib_plankton.xml.class_node_complex( "table", { }, [ new lib_plankton.xml.class_node_complex( "thead", { }, [ new lib_plankton.xml.class_node_complex( "tr", { }, ( [ new lib_plankton.xml.class_node_complex( "th", { "class": "calendar-cell", }, [ ] ), ] .concat( ["Mo","Di","Mi","Do","Fr","Sa","So"] .map( (day) => new lib_plankton.xml.class_node_complex( "th", { "class": "calendar-cell calendar-cell-day", }, [ new lib_plankton.xml.class_node_text(day) ] ) ) ) ) ) ] ), new lib_plankton.xml.class_node_complex( "tbody", { }, ( stuff .map( (row) => ( new lib_plankton.xml.class_node_complex( "tr", { }, ( [ new lib_plankton.xml.class_node_complex( "th", { "class": "calendar-cell calendar-cell-week", }, [ new lib_plankton.xml.class_node_text( row.week.toFixed(0).padStart(2, "0") ) ] ), ] .concat( row.data .map( (cell) => ( new lib_plankton.xml.class_node_complex( "td", { "class": ( ( ["calendar-cell", "calendar-cell-regular"] .concat(cell.today ? ["calendar-cell-today"] : []) ) .join(" ") ), "title": lib_plankton.call.convey( cell.pit, [ pit_to_datetime, (x : type_datetime) => lib_plankton.string.coin( "{{year}}-{{month}}-{{day}}", { "year": x.date.year.toFixed(0).padStart(4, "0"), "month": x.date.month.toFixed(0).padStart(2, "0"), "day": x.date.day.toFixed(0).padStart(2, "0"), } ), ] ), }, [ new lib_plankton.xml.class_node_complex( "span", { "class": "calendar-day", }, [ new lib_plankton.xml.class_node_text( lib_plankton.call.convey( cell.pit, [ pit_to_datetime, (x : type_datetime) => lib_plankton.string.coin( "{{day}}", { "year": x.date.year.toFixed(0).padStart(4, "0"), "month": x.date.month.toFixed(0).padStart(2, "0"), "day": x.date.day.toFixed(0).padStart(2, "0"), } ), ] ) ) ] ), new lib_plankton.xml.class_node_complex( "ul", { "class": "calendar-events", }, ( cell.entries .map( entry => ( new lib_plankton.xml.class_node_complex( "li", { "class": "calendar-event_entry", "style": lib_plankton.string.coin( "background-color: {{color}}", { "color": lib_plankton.call.convey( entry.calendar_id, [ (n : int) => ({"n": n, "saturation": 0.25, "value": 0.5}), lib_plankton.color.give_generic, lib_plankton.color.output_hex, ] ), } ), "title": ( lib_plankton.string.coin( "[{{calendar_name}}] {{event_name}}\n", { "calendar_name": entry.calendar_id.toFixed(0), "event_name": entry.event.name, } ) + "--\n" + ( (entry.event.begin.time !== null) ? lib_plankton.string.coin( "{{label}}: {{value}}\n", { "label": "Anfang", // TODO: translate "value": lib_plankton.string.coin( "{{hour}}:{{minute}}", { "hour": entry.event.begin.time.hour.toFixed(0).padStart(2, "0"), "minute": entry.event.begin.time.minute.toFixed(0).padStart(2, "0"), } ), // TODO: outsource } ) : "" ) + ( (entry.event.end !== null) ? lib_plankton.string.coin( "{{label}}: {{value}}\n", { "label": "Ende", // TODO: translate "value": ( [ ( ( (entry.event.end.date.year !== entry.event.begin.date.year) || (entry.event.end.date.month !== entry.event.begin.date.month) || (entry.event.end.date.day !== entry.event.begin.date.day) ) ? lib_plankton.string.coin( "{{year}}-{{month}}-{{day}}", { "year": entry.event.end.date.year.toFixed(0).padStart(4, "0"), "month": entry.event.end.date.month.toFixed(0).padStart(2, "0"), "day": entry.event.end.date.month.toFixed(0).padStart(2, "0"), } ) : null ), ( (entry.event.end.time !== null) ? lib_plankton.string.coin( "{{hour}}:{{minute}}", { "hour": entry.event.end.time.hour.toFixed(0).padStart(2, "0"), "minute": entry.event.end.time.minute.toFixed(0).padStart(2, "0"), } ) : null ), ] .filter(x => (x !== null)) .join(",") ), } ) : "" ) + ( (entry.event.description !== null) ? ( "--\n" + lib_plankton.string.coin( "{{description}}\n", { "description": ( (entry.event.description !== null) ? entry.event.description : "?" ), } ) ) : "" ) ), }, [ new lib_plankton.xml.class_node_text(entry.event.name), ] ) ) ) ) ), ] ) ) ) ) ) ) ) ) ) ) ] ) ] ).compile() ); } /** */ async function calendar_view_list_data( data : type_datamodel, calendar_id : type_calendar_id, options : { from ?: type_pit; to ?: type_pit; timezone_shift ?: int; } = {} ) : Promise< Array< { calendar_id : type_calendar_id; event : type_event; } > > { const now_pit : type_pit = pit_now(); options = Object.assign( { "from": lib_plankton.call.convey( now_pit, [ (x : type_pit) => pit_shift_day(x, -1), ] ), "to": lib_plankton.call.convey( now_pit, [ (x : type_pit) => pit_shift_week(x, +4), ] ), "timezone_shift": 0, }, options ); const entries : Array< { calendar_id : type_calendar_id; event : type_event; } > = await calendar_gather_events( data, calendar_id, (options.from as type_pit), (options.to as type_pit) ); // TODO: optimize entries.sort( (entry_1, entry_2) => (pit_from_datetime(entry_1.event.begin) - pit_from_datetime(entry_2.event.begin)) ); return Promise.resolve(entries); } /** */ async function calendar_view_list_html( data : type_datamodel, calendar_id : type_calendar_id, options : { from ?: type_pit; to ?: type_pit; timezone_shift ?: int; } = {} ) : Promise { const stuff : Array< { calendar_id : type_calendar_id; event : type_event; } > = await calendar_view_list_data( data, calendar_id, options ); return Promise.resolve( new lib_plankton.xml.class_node_complex( "div", { "class": "list", }, [ new lib_plankton.xml.class_node_complex( "style", {}, [ new lib_plankton.xml.class_node_text( "html {background-color: #111; color: #FFF; font-family: sans-serif;}\n" + "table {width: 100%; border-collapse: collapse;}\n" ) ] ), new lib_plankton.xml.class_node_complex( "ul", { "class": "list-events", }, ( stuff .map( (entry) => ( new lib_plankton.xml.class_node_complex( "li", { "class": "list-event_entry", }, [ new lib_plankton.xml.class_node_text( JSON.stringify(entry) ), ] ) ) ) ) ), ] ).compile() ); }