diff --git a/conf/example.json b/conf/example.json index 4db7b20..63ec3b1 100644 --- a/conf/example.json +++ b/conf/example.json @@ -1,5 +1,4 @@ { "view_mode": "table", - "calendar_id": 6, "timezone_shift": 0 } diff --git a/data/example.kal.json b/data/example.kal.json index 47cc274..e0b12aa 100644 --- a/data/example.kal.json +++ b/data/example.kal.json @@ -63,7 +63,7 @@ ], "calendars": [ { - "id": 5, + "id": 1, "object": { "kind": "concrete", "data": { @@ -123,7 +123,7 @@ } }, { - "id": 3, + "id": 2, "object": { "kind": "concrete", "data": { @@ -198,7 +198,7 @@ } }, { - "id": 4, + "id": 3, "object": { "kind": "concrete", "data": { @@ -226,7 +226,7 @@ } }, { - "id": 1, + "id": 4, "object": { "kind": "concrete", "data": { @@ -283,7 +283,7 @@ } }, { - "id": 2, + "id": 5, "object": { "kind": "concrete", "data": { @@ -333,27 +333,11 @@ }, { "id": 6, - "object": { - "kind": "collection", - "data": { - "name": "Überblick", - "sources": [ - 1, - 2, - 3, - 4, - 5 - ] - } - } - }, - { - "id": 7, "object": { "kind": "caldav", "data": { "name": "Lixer", - "private": false, + "private": true, "read_only": true, "source_url": "https://export.kalender.digital/ics/0/3e10dae66950379d4cc8/gesamterkalender.ics?past_months=3&future_months=36" } diff --git a/doc/konzept.md b/doc/konzept.md index e8bb6fa..34ba108 100644 --- a/doc/konzept.md +++ b/doc/konzept.md @@ -1,5 +1,6 @@ - Kalender sollen unabhängig von Nutzern bestehen können - einem Kalender können beliebig viele Nutzer zugeordnet werden, die jeweils bestimmte Berechtigungen haben (z.B. als Rollen "admin", "editor", "viewer", …) +- Events bilden keine Domäne - Berechtigungen: - Kalender anlegen - Kalender-Stammdaten ändern @@ -11,11 +12,10 @@ - es gibt verschiedene Arten von Kalendern: - konkret - enthält Veranstaltungen - - Sammlung - - enthält selbst keine Veranstaltungen - - verweist auf beliebig viele andere Kalender (Kreise vermeiden) - extern - über CalDAV - sollte read-only- und read/write-Modus haben -- nach dem Anmelden sieht man eine Liste mit öffentlichen Kalendern, gruppiert nach jewiliger Rolle +- nach dem Anmelden sieht man eine Kalender-Ansicht mit folgenden Kalendern kombiniert angezeigt: + - öffentliche Kalender + - nicht öffentliche Kalendar, bei welchen man Lese-Berechtigung hat - Entwurfsname: "zeitbild" diff --git a/source/logic/backend.ts b/source/logic/backend.ts index 7a71b2c..ccb2f05 100644 --- a/source/logic/backend.ts +++ b/source/logic/backend.ts @@ -130,14 +130,6 @@ namespace _zeitbild.frontend.resources.backend }; break; } - case "collection": { - return { - "key": calendar_entry.id, - "preview": { - "name": calendar_entry.object.data.name, - } - }; - } case "caldav": { return { "key": calendar_entry.id, @@ -179,7 +171,7 @@ namespace _zeitbild.frontend.resources.backend * @todo prevent loops */ export async function calendar_gather_events( - calendar_id : type_calendar_id, + calendar_ids : Array, from_pit : _zeitbild.frontend.helpers.type_pit, to_pit : _zeitbild.frontend.helpers.type_pit ) : Promise< @@ -192,148 +184,150 @@ namespace _zeitbild.frontend.resources.backend > > { - await init(); - const calendar_object : type_calendar_object = await calendar_read( - calendar_id - ); lib_plankton.log.info( "calendar_gather_events", { - "calendar_id": calendar_id, + "calendar_ids": calendar_ids, } ); - switch (calendar_object.kind) { - case "concrete": { - return Promise.resolve( - calendar_object.data.events - .filter( - (event) => _zeitbild.frontend.helpers.pit_is_between( - _zeitbild.frontend.helpers.pit_from_datetime(event.begin), - from_pit, - to_pit - ) - ) - .map( - (event) => ({ - "calendar_id": calendar_id, - "calendar_name": calendar_object.data.name, - "event": event - }) - ) - ); - break; + await init(); + let result : Array< + { + calendar_id : type_calendar_id; + calendar_name : string; + event : type_event; } - case "collection": { - return ( - Promise.all( - calendar_object.data.sources - .map( - (source_calendar_id) => calendar_gather_events( - source_calendar_id, - from_pit, - to_pit - ) - ) - ) - .then( - (entries) => Promise.resolve( - entries - .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/2", - "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, + > = []; + for await (const calendar_id of calendar_ids) { + const calendar_object : type_calendar_object = await calendar_read( + calendar_id + ); + if (calendar_object.data.private) { + lib_plankton.log.info( + "calendar_gather_events_private_calendar_blocked", { + "calendar_id": calendar_id, } ); - const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode( - http_response.body.toString(), - { + } + else { + switch (calendar_object.kind) { + case "concrete": { + result = ( + result + .concat( + calendar_object.data.events + .filter( + (event) => _zeitbild.frontend.helpers.pit_is_between( + _zeitbild.frontend.helpers.pit_from_datetime(event.begin), + from_pit, + to_pit + ) + ) + .map( + (event) => ({ + "calendar_id": calendar_id, + "calendar_name": calendar_object.data.name, + "event": event + }) + ) + ) + ); + break; } - ); - return Promise.resolve( - vcalendar.vevents - .map( - (vevent : lib_plankton.ical.type_vevent) => ( - (vevent.dtstart !== undefined) - ? + 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/2", + "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, { - "name": ( - (vevent.summary !== undefined) - ? - vevent.summary - : - "???" - ), - "begin": _zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtstart), - "end": ( - (vevent.dtend !== undefined) - ? - _zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtend) - : - null - ), - "location": ( - (vevent.location !== undefined) - ? - vevent.location - : - null - ), - "description": ( - (vevent.description !== undefined) - ? - vevent.description - : - null - ), } - : - null - ) - ) - .filter( - (event) => (event !== null) - ) - .filter( - (event) => _zeitbild.frontend.helpers.pit_is_between( - _zeitbild.frontend.helpers.pit_from_datetime(event.begin), - from_pit, - to_pit - ) - ) - .map( - (event) => ({ - "calendar_id": calendar_id, - "calendar_name": calendar_object.data.name, - "event": event, - }) - ) - ); - break; + ); + const vcalendar : lib_plankton.ical.type_vcalendar = lib_plankton.ical.ics_decode( + http_response.body.toString(), + { + } + ); + result = ( + result + .concat( + vcalendar.vevents + .map( + (vevent : lib_plankton.ical.type_vevent) => ( + (vevent.dtstart !== undefined) + ? + { + "name": ( + (vevent.summary !== undefined) + ? + vevent.summary + : + "???" + ), + "begin": _zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtstart), + "end": ( + (vevent.dtend !== undefined) + ? + _zeitbild.frontend.helpers.ical_dt_to_own_datetime(vevent.dtend) + : + null + ), + "location": ( + (vevent.location !== undefined) + ? + vevent.location + : + null + ), + "description": ( + (vevent.description !== undefined) + ? + vevent.description + : + null + ), + } + : + null + ) + ) + .filter( + (event) => (event !== null) + ) + .filter( + (event) => _zeitbild.frontend.helpers.pit_is_between( + _zeitbild.frontend.helpers.pit_from_datetime(event.begin), + from_pit, + to_pit + ) + ) + .map( + (event) => ({ + "calendar_id": calendar_id, + "calendar_name": calendar_object.data.name, + "event": event, + }) + ) + ) + ); + break; + } + } } } + return Promise.resolve(result); } } diff --git a/source/logic/main.ts b/source/logic/main.ts index 35b3327..4a123f4 100644 --- a/source/logic/main.ts +++ b/source/logic/main.ts @@ -7,11 +7,87 @@ namespace _zeitbild.frontend */ type type_conf = { view_mode : string; - calendar_id : int; + calendar_ids : Array; timezone_shift : int; }; + /** + */ + async function render( + conf : type_conf, + calendar_ids : Array + ) : Promise + { + calendar_ids.sort(); + const target : HTMLElement = (document.querySelector("body") as HTMLBodyElement); + switch (conf.view_mode) { + default: { + throw (new Error("invalid view mode")); + break; + } + case "table": { + const content : string = await _zeitbild.frontend.view.calendar_view_table_html( + calendar_ids, + { + "from": { + "year": 2024, + "week": 35 + }, + "to": { + "year": 2024, + "week": 43 + }, + "timezone_shift": conf.timezone_shift, + } + ); + target.innerHTML = content; + /* + document.querySelectorAll(".tableview-sources-entry").forEach( + (element) => { + element.addEventListener( + "click", + (event) => { + const element_ : HTMLElement = (event.target as HTMLElement); + const calendar_id : type_calendar_id = parseInt(element_.getAttribute("rel") as string); + const active : boolean = element_.classList.toggle("tableview-sources-entry-active"); + render( + conf, + lib_plankton.call.convey( + calendar_ids, + [ + (x : Array) => ( + active + ? + calendar_ids.concat([calendar_id]) + : + calendar_ids.filter(y => (y !== calendar_id)) + ), + ] + ) + ); + } + ); + } + ); + */ + break; + } + case "list": { + const content : string = await _zeitbild.frontend.view.calendar_view_list_html( + calendar_ids, + { + "timezone_shift": conf.timezone_shift, + } + ); + target.innerHTML = content; + break; + } + } + return Promise.resolve(undefined); + } + + /** */ export async function main( @@ -27,42 +103,29 @@ namespace _zeitbild.frontend // conf const conf : type_conf = lib_plankton.json.decode(await lib_plankton.file.read("conf.json")); + // args + + const url : URL = new URL(window.location.toString()); + const calendar_ids : Array = ( + (url.searchParams.get("ids") !== null) + ? + lib_plankton.call.convey( + url.searchParams.get("ids"), + [ + (x : string) => lib_plankton.string.split(x, ","), + (x : Array) => x.map(y => parseInt(y)), + ] + ) + : + (await _zeitbild.frontend.resources.backend.calendar_list()).map(x => x.key) + ); + // exec - let content : string; - switch (conf.view_mode) { - default: { - content = ""; - throw (new Error("invalid view mode")); - break; - } - case "table": { - content = await _zeitbild.frontend.view.calendar_view_table_html( - conf.calendar_id, - { - "from": { - "year": 2024, - "week": 35 - }, - "to": { - "year": 2024, - "week": 43 - }, - "timezone_shift": conf.timezone_shift, - } - ); - break; - } - case "list": { - content = await _zeitbild.frontend.view.calendar_view_list_html( - conf.calendar_id, - { - "timezone_shift": conf.timezone_shift, - } - ); - break; - } - } - (document.querySelector("body") as HTMLBodyElement).innerHTML = content; + await render( + conf, + calendar_ids + ); + return Promise.resolve(undefined); } diff --git a/source/logic/types.ts b/source/logic/types.ts index ac17b04..1cff1b8 100644 --- a/source/logic/types.ts +++ b/source/logic/types.ts @@ -3,7 +3,7 @@ */ namespace _zeitbild.frontend { - + /** */ type type_role = ( @@ -11,8 +11,8 @@ namespace _zeitbild.frontend | "viewer" ); - - + + /** */ type type_user_id = int; @@ -46,13 +46,13 @@ namespace _zeitbild.frontend string ); }; - - + + /** */ export type type_calendar_id = int; - - + + /** * @todo bei "collection" Kreise vermeiden */ @@ -72,17 +72,6 @@ namespace _zeitbild.frontend }; } | - { - kind : "collection"; - data : { - name : string; - private : boolean; - sources : Array< - type_calendar_id - >; - } - } - | { kind : "caldav"; data : { @@ -93,8 +82,8 @@ namespace _zeitbild.frontend } } ); - - + + /** */ export type type_datamodel = { diff --git a/source/logic/view.ts b/source/logic/view.ts index b44f838..48db65c 100644 --- a/source/logic/view.ts +++ b/source/logic/view.ts @@ -65,7 +65,7 @@ namespace _zeitbild.frontend.view { "year": event.end.date.year.toFixed(0).padStart(4, "0"), "month": event.end.date.month.toFixed(0).padStart(2, "0"), - "day": event.end.date.month.toFixed(0).padStart(2, "0"), + "day": event.end.date.day.toFixed(0).padStart(2, "0"), } ) : @@ -134,7 +134,7 @@ namespace _zeitbild.frontend.view * @todo kein "while" */ async function calendar_view_table_data( - calendar_id : type_calendar_id, + calendar_ids : Array, options : { from ?: { year : int; @@ -232,7 +232,7 @@ namespace _zeitbild.frontend.view event : type_event; } > = await _zeitbild.frontend.resources.backend.calendar_gather_events( - calendar_id, + calendar_ids, from_pit, to_pit ); @@ -389,7 +389,7 @@ namespace _zeitbild.frontend.view /** */ export async function calendar_view_table_html( - calendar_id : type_calendar_id, + calendar_ids : Array, options : { from ?: { year : int; @@ -428,7 +428,7 @@ namespace _zeitbild.frontend.view } >; } = await calendar_view_table_data( - calendar_id, + calendar_ids, options ); const sources : lib_plankton.structures.type_hashmap< @@ -448,7 +448,7 @@ namespace _zeitbild.frontend.view "value": { "name": pair.value.name, "color": lib_plankton.color.give_generic( - (pair.key + 0.2), + (pair.key - 1), { "saturation": 0.375, "value": 0.375, @@ -470,6 +470,7 @@ namespace _zeitbild.frontend.view { "name": data.name, "color": lib_plankton.color.output_hex(data.color), + "rel": calendar_id.toFixed(0), } ) ) @@ -567,7 +568,7 @@ namespace _zeitbild.frontend.view /** */ async function calendar_view_list_data( - calendar_id : type_calendar_id, + calendar_ids : Array, options : { from ?: _zeitbild.frontend.helpers.type_pit; to ?: _zeitbild.frontend.helpers.type_pit; @@ -608,7 +609,7 @@ namespace _zeitbild.frontend.view event : type_event; } > = await _zeitbild.frontend.resources.backend.calendar_gather_events( - calendar_id, + calendar_ids, (options.from as _zeitbild.frontend.helpers.type_pit), (options.to as _zeitbild.frontend.helpers.type_pit) ); @@ -628,7 +629,7 @@ namespace _zeitbild.frontend.view /** */ export async function calendar_view_list_html( - calendar_id : type_calendar_id, + calendar_ids : Array, options : { from ?: _zeitbild.frontend.helpers.type_pit; to ?: _zeitbild.frontend.helpers.type_pit; @@ -642,7 +643,7 @@ namespace _zeitbild.frontend.view event : type_event; } > = await calendar_view_list_data( - calendar_id, + calendar_ids, options ); return Promise.resolve( diff --git a/source/style/main.css b/source/style/main.css index aeabb9b..61cd645 100644 --- a/source/style/main.css +++ b/source/style/main.css @@ -22,11 +22,17 @@ html { margin: 0; padding: 0; list-style-type: none; + font-size: 0.75em; } .tableview-sources-entry { margin: 8px; padding: 4px; + cursor: pointer; +} + +.tableview-sources-entry:not(.tableview-sources-entry-active) { + filter: saturate(0); } .calendar table { diff --git a/source/templates/tableview-sources-entry.html.tpl b/source/templates/tableview-sources-entry.html.tpl index a78d64c..861ea42 100644 --- a/source/templates/tableview-sources-entry.html.tpl +++ b/source/templates/tableview-sources-entry.html.tpl @@ -1 +1 @@ -
  • {{name}}
  • +
  • {{name}}