calendar
All repositories: gitoria
9.2 KB
// events.hl — THE EVENTS (ticket calendar#2). One hl:mpackdb table, storage/mpackdb/events.db, pk @id, index owner.// A record IS a WorldAPI Event (https://worldapi.org/standards/event): the `$`-keys `$type $version $kind $title// $description $start $end $timezone $location $status $visibility $created $updated`, plus `owner` (the users @id)// and the @id. Appointment: `$start`/`$end` local wall-clock `YYYY-MM-DDTHH:MM:SS` in `$timezone`. All-day:// `$kind = 'allday'`, `$start` a date, `$end` the EXCLUSIVE end date (left out for one day). Every event is// `$visibility = 'private'`: every function takes the owner's user id and reaches only that user's events.import { MPackDB } from 'hl:mpackdb'import { timestamp } from 'hl:time'import { storageDir, countOfList, firstOf } from './users.hl'import { isText, isDate, isTime, isZone, nextDay, prevDay, before } from './components/month-view/dates.hl'import { toReminders, toRows, defaultRows } from './components/month-view/reminders.hl'static eventsTable = new MPackDB(file = storageDir + '/events.db', primaryKey = '@id', indexes = ['owner'])// the calendar's settings: the default reminders every event without its own gets (`$reminders` of the WorldAPI Calendar)static settingsTable = new MPackDB(file = storageDir + '/settings.db', primaryKey = '@id', indexes = ['!owner'])static maxTitle = 200static maxPlace = 200static maxNotes = 5000// what the page sends: { id?, custom, reminders (rows), title, allDay, startDate, startTime, endDate, endTime, place, notes, zone }// → { error } or { record } (a WorldAPI Event without owner/id). endDate/endTime may be empty (no end); all-day: the// last day, inclusive ('' = one day).static build = (b) => {if (b == null || hlTypeName(b) != 'Hybrid') { return { error = 'the event must be an object' } }if (!isText(b.title) || b.title.trim() == '') { return { error = 'the event needs a title' } }if (b.title.length > maxTitle) { return { error = 'the title is too long (at most ' + maxTitle + ' characters)' } }if (!isText(b.place) || b.place.length > maxPlace) { return { error = 'the place is too long (at most ' + maxPlace + ' characters)' } }if (!isText(b.notes) || b.notes.length > maxNotes) { return { error = 'the notes are too long (at most ' + maxNotes + ' characters)' } }if (b.allDay != true && b.allDay != false) { return { error = 'allDay must be true or false' } }if (!isDate(b.startDate)) { return { error = 'the start date is not a date' } }if (!isZone(b.zone)) { return { error = 'the time zone is missing' } }let r = {}r['$type'] = 'event'r['$version'] = 1r['$title'] = b.title.trim()if (b.notes.trim() != '') { r['$description'] = b.notes }if (b.place.trim() != '') { r['$location'] = b.place.trim() }r['$status'] = 'confirmed'r['$visibility'] = 'private'// no own reminders: `$reminders` stays out and the calendar's apply; own ones (also none at all) are writtenif (b.custom == true) {let rem = toReminders(b.reminders)if (rem.error != null) { return { error = rem.error } }r['$reminders'] = rem.reminders}if (b.allDay) {r['$kind'] = 'allday'r['$start'] = b.startDateif (isText(b.endDate) && b.endDate != '') {if (!isDate(b.endDate)) { return { error = 'the end date is not a date' } }if (before(b.endDate, b.startDate)) { return { error = 'the end is before the start' } }if (b.endDate != b.startDate) { r['$end'] = nextDay(b.endDate) }}return { record = r }}if (!isTime(b.startTime)) { return { error = 'the start time is not a time' } }r['$kind'] = 'appointment'r['$timezone'] = b.zoner['$start'] = b.startDate + 'T' + b.startTime + ':00'let hasEnd = (isText(b.endDate) && b.endDate != '') || (isText(b.endTime) && b.endTime != '')if (hasEnd) {let ed = isText(b.endDate) && b.endDate != '' ? b.endDate : b.startDateif (!isDate(ed)) { return { error = 'the end date is not a date' } }if (!isTime(b.endTime)) { return { error = 'the end time is not a time' } }let end = ed + 'T' + b.endTime + ':00'if (before(end, r['$start'])) { return { error = 'the end is before the start' } }r['$end'] = end}return { record = r }}// what the page shows of a stored event: first / last day (inclusive), times, textsstatic viewOf = (e) => {let allDay = e['$kind'] == 'allday'let start = e['$start']let end = e['$end']let first = start.slice(0, 10)let last = firstif (allDay) {if (end != null) { last = prevDay(end) }} else if (end != null) {last = end.slice(0, 10)}let v = { id = e.id title = e['$title'] allDay = allDay first = first last = laststartDate = first startTime = allDay ? '' : start.slice(11, 16)endDate = end == null ? '' : last endTime = allDay || end == null ? '' : end.slice(11, 16)place = e['$location'] == null ? '' : e['$location'] notes = e['$description'] == null ? '' : e['$description']start = start zone = e['$timezone'] == null ? '' : e['$timezone']custom = e['$reminders'] != null rows = e['$reminders'] == null ? [] : toRows(e['$reminders']) }return v}static ownEvent = (userId, id) => {if (id == null || hlTypeName(id) != 'String' || id == '') { return null }let e = eventsTable.fetch(id)if (e == null || e.owner != userId) { return null }return e}// the owner's events that touch first..last (dates), by startstatic eventsBetween = (userId, first, last) => {let found = eventsTable.find('owner', userId)let n = countOfList(found)let views = []let i = 0while (i < n) {let v = viewOf(found[i])if (!before(v.last, first) && !before(last, v.first)) { views.push(v) }i = i + 1}let m = views.lengthlet out = []let used = []i = 0while (i < m) { used.push(false) i = i + 1 }let round = 0while (round < m) {let best = -1let j = 0while (j < m) {if (!used[j] && (best < 0 || before(views[j].start, views[best].start))) { best = j }j = j + 1}used[best] = trueout.push(views[best])round = round + 1}return out}// an event made offline carries a temporary id `local-…` until the page reloads; the server keeps it as `ref`, so an edit// (or delete) sent with that id reaches the same event instead of making a second onestatic isLocalId = (id) => { return isText(id) && id.length > 6 && id.length <= 60 && id.slice(0, 6) == 'local-' }static realId = (userId, id) => {if (!isLocalId(id)) { return id }let found = eventsTable.find('owner', userId)let n = countOfList(found)let i = 0while (i < n) {if (found[i].ref == id) { return found[i].id }i = i + 1}return null}// create, or change when the fields name an event (real or temporary id)static saveEvent = (userId, b) => {if (b != null && hlTypeName(b) == 'Hybrid' && b.id != null && b.id != '') {let id = realId(userId, b.id)if (id != null) { return changeEvent(userId, id, b) }if (!isLocalId(b.id)) { return changeEvent(userId, b.id, b) }}return createEvent(userId, b)}static createEvent = (userId, b) => {let x = build(b)if (x.error != null) { return x }let r = x.recordlet t = timestamp()r['$created'] = tr['$updated'] = tr.owner = userIdif (isLocalId(b.id)) { r.ref = b.id }let id = eventsTable.put(r)if (id == null) { return { error = 'could not store the event: ' + eventsTable.lastError() } }return { id = id }}static changeEvent = (userId, id, b) => {let old = ownEvent(userId, id)if (old == null) { return { error = 'no such event' } }let x = build(b)if (x.error != null) { return x }let r = x.recordr['$created'] = old['$created']r['$updated'] = timestamp()r.owner = userIdif (old.ref != null) { r.ref = old.ref }r.id = ideventsTable.update(id, r)return { id = id }}static deleteEvent = (userId, given) => {let id = realId(userId, given)let old = ownEvent(userId, id)if (old == null) { return { error = 'no such event' } }eventsTable.delete(id)return { ok = true }}// ---- the calendar's default reminders and the reminders still to come ----------------------------------static settingsOf = (userId) => { return firstOf(settingsTable.find('owner', userId)) }// the rows of the owner's defaults (1 hour before, the day before at 20:00 until they change them)static defaultsOf = (userId) => {let s = settingsOf(userId)if (s == null || s.reminders == null) { return defaultRows }return toRows(s.reminders)}static saveDefaults = (userId, rows) => {let rem = toReminders(rows)if (rem.error != null) { return rem }let s = settingsOf(userId)let r = { owner = userId reminders = rem.reminders }if (s == null) {if (settingsTable.put(r) == null) { return { error = 'could not store the settings: ' + settingsTable.lastError() } }} else {r.id = s.idsettingsTable.update(s.id, r)}return { ok = true }}// the owner's events starting first..last with the reminders that apply (own, else the defaults); timed and all-daystatic upcoming = (userId, first, last) => {let defaults = defaultsOf(userId)let found = eventsTable.find('owner', userId)let n = countOfList(found)let out = []let i = 0while (i < n) {let v = viewOf(found[i])if (!before(v.first, first) && !before(last, v.first)) {let list = { id = v.id title = v.title allDay = v.allDay startDate = v.startDate startTime = v.startTime place = v.place zone = v.zone rows = v.custom ? v.rows : defaults }out.push(list)}i = i + 1}return out}
Branches
- mainmain branch
Latest commits
- 76edaa62calendar: Hybriel master ff51cf46 (re-vendor round, static workaround removed)mre
- 90a3fc2cdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 6722b72ddeploy.sh: never send .git or .gitignore to Byrodinmre
- be099807State of 2026-09-27, before the move to gitoriamre