# Calendar Editor A booking calendar on capacity: week / day / month views, mobile agenda, occupancy with buffers, blocked intervals, opening hours, in-panel create / reschedule / cancel, and round-trip import / export in **JSON** (`.calendar`) and **iCalendar** (`.ics`). Built on top of the [`gadgets/calendar`](https://cdn.sgapps.io/components/application-prototype-thirdparty/docs/#/gadgets/calendar.md) module (SGApps.IO product: written in-house, no third-party code, no network of its own). The editor adds a save/export toolbar, settings dialogs for resource / opening hours / blocked intervals, and acts as its own host adapter -- creates / reschedules / cancellations are written straight back into the in-memory event list so the editor is self-sufficient out of the box. ## Live Demo The demo opens with a sample `.ics` file (three service-bay bookings around 26-28 October 2026). Navigate with ◀ / ▶ or **Today** -- if you don't see them, press **Today** then ◀ to find the week. ## Embed ```html ``` ### Open with a file The first argument becomes the file (local file-manager path OR an `http(s)://` URL). The extension picks the parser: | Extension | Format | Round-trip | |---|---|---| | `.calendar`, `.json` (JSON content) | SGApps.IO in-house schema | ✅ full -- resource, opening hours, statuses, blocks, events, locale, zones | | `.ics`, `.vcs` | RFC 5545 iCalendar | ✅ events only; resource / opening hours / custom statuses lost (not in the standard) | ```html ``` ### In-page (no iframe) ```html ``` --- ## Events Reference ```js var socket = new WindowSocket(iframe.contentWindow); socket.start(); socket.fire("webapp::instance::request", eventName, ...args, callback); ``` The calendar also emits events upward through `api-event::instance:*`; see the full list under **Calendar events (upstream)** below. --- ### `getEvents`, `setEvents`, `upsertEvent`, `removeEvent` ```js socket.fire("webapp::instance::request", "upsertEvent", { id: "k-99", start: "2026-10-26T07:30:00Z", end: "2026-10-26T09:00:00Z", title: "New booking", status: "open", quantity: 1 }, function (err, id) {}); ``` --- ### `setView`, `getView`, `today`, `prev`, `next`, `setDate` ```js socket.fire("webapp::instance::request", "setView", "month", function (err) {}); socket.fire("webapp::instance::request", "setDate", "2026-10-26", function (err) {}); ``` --- ### `setResource`, `setCapacity`, `setOpeningHours` ```js socket.fire("webapp::instance::request", "setCapacity", 3, function (err) {}); socket.fire("webapp::instance::request", "setOpeningHours", { 0: null, 1: [["09:00", "18:00"]], 2: [["09:00", "18:00"]], 3: [["09:00", "18:00"]], 4: [["09:00", "18:00"]], 5: [["09:00", "18:00"]], 6: null }, function (err) {}); ``` --- ### `setBlocks`, `getBlocks`, `addBlock`, `removeBlock` ```js // Absolute block socket.fire("webapp::instance::request", "addBlock", { id: "maint-1", start: "2026-10-26T09:00:00Z", end: "2026-10-26T10:00:00Z", label: "Maintenance", kind: "deny" }, function (err) {}); // Recurring daily lunch socket.fire("webapp::instance::request", "addBlock", { id: "lunch", weekday: -1, startTime: "12:00", endTime: "13:00", label: "Lunch break", kind: "deny", active: true }, function (err) {}); ``` --- ### `setLocale`, `setTimeZone`, `setReadOnly` Languages shipped: `en`, `ro`, `ru`, `es`, `fr`, `it`, `de`, `zh`. See [`gadgets/calendar` §Languages](https://cdn.sgapps.io/components/application-prototype-thirdparty/docs/#/gadgets/calendar.md) for plural rules + conventions per locale. ```js socket.fire("webapp::instance::request", "setLocale", "ro", function (err) {}); socket.fire("webapp::instance::request", "setTimeZone", "Europe/Chisinau", "Europe/Chisinau", function (err) {}); ``` --- ### `openCreate`, `selectEvent`, `closePanel` ```js socket.fire("webapp::instance::request", "openCreate", "2026-10-26T10:00:00Z", "2026-10-26T11:00:00Z", function (err) {}); ``` --- ### `dayStats`, `rangeStats`, `findSlots` ```js socket.fire("webapp::instance::request", "rangeStats", function (err, s) { console.log(s); }); socket.fire("webapp::instance::request", "findSlots", { durationMinutes: 90, quantity: 1, count: 3, stepMinutes: 90 }, function (err, slots) { console.log(slots); }); ``` --- ### `exportAs`, `toICS` ```js socket.fire("webapp::instance::request", "exportAs", "ics", function (err, text) { var blob = new Blob([text], { type: "text/calendar" }); var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "bookings.ics"; a.click(); }); ``` --- ### `save`, `saveAs` Writes the current calendar back to the file it was opened with: ```js socket.fire("webapp::instance::request", "save", function (err) {}); socket.fire("webapp::instance::request", "saveAs", "ics", "/home/me/bookings.ics", function (err) {}); ``` Call `saveAs` with `null` filename to open the file-selector dialog. The toolbar's **Save** falls back to Save-As on the first save of a URL-opened calendar. --- ## Calendar events (upstream) Forwarded through `api-event::instance:*`: ```js socket.on("api-event::instance:event:create-requested", function (draft) { // the user confirmed a new booking in the panel; the editor's // built-in host adapter already wrote it in-memory. Replace this // adapter in your own code to send it to your server first. }); ``` | Event | Payload | |---|---| | `ready` | the instance | | `event:range-changed` | `{view, start, end, days}` | | `event:view-changed` | `{view}` | | `event:day-focused` | `{date}` | | `event:scope-filter` | `{scope}` | | `event:status-filter` | `{hidden: []}` | | `event:event-selected` | the booking, with its `data` | | `event:event-deselected` | `{id}` | | `event:draft-changed` | the draft, on every panel change | | `event:create-requested` | `{start, end, date, durationMinutes, quantity, scope, title, token, override}` | | `event:reschedule-requested` | `{id, start, end, base: {start, end}, quantity, data}` | | `event:cancel-requested` | `{id, reason, note, data}` | | `event:error` | `{code, message, error}` | --- ## Data shapes **A booking** (what `upsertEvent` takes and `getEvents` returns): ```js { id: "k-5", start: "2026-10-26T07:30:00Z", // ISO 8601 UTC end: "2026-10-26T09:00:00Z", quantity: 1, // capacity units; decimals allowed title: "Aroma Coffee Shop Ltd", subtitle: null, description: "Full espresso machine service + descaling", code: "K-5", status: "open", // key of statuses scope: null, bufferBefore: 15, // minutes held before start bufferAfter: 15, editable: true, data: { anything: "the host wants back" } } ``` **A blocked interval** -- absolute: ```js { id: "maint-1", start: "2026-10-26T09:00:00Z", end: "2026-10-26T10:00:00Z", label: "Maintenance", kind: "deny" } ``` Or recurring (wall-clock time in the business zone): ```js { id: "lunch", weekday: -1, startTime: "12:00", endTime: "13:00", label: "Lunch", kind: "deny", active: true } ``` `weekday: -1` = every day; `0` = Sunday, …, `6` = Saturday. `kind: "allow"` inverts it into an availability window. --- ## Use Cases ### A booking widget on a landing page Embed with `readOnly: true` on load, use `addBlock` for recurring breaks and let the user see when the resource is free -- no risk of them creating a stray booking. ### Resource planner for an operations dashboard Open with `setView("month")`, call `rangeStats` on `event:range-changed` to surface peak and percentage per week. ### Import an exported calendar from another system Save it as `.ics`, point the editor at it via `/online/webapp/calendar-editor/url/`. VEVENT blocks are parsed; resource / opening hours stay at the editor's defaults. ### Build your own write backend The editor ships with a built-in host adapter that writes to its own memory. For a server-backed workflow: 1. Set `readOnly: false` (default). 2. Listen for `api-event::instance:event:create-requested` / `event:reschedule-requested` / `event:cancel-requested`. 3. POST to your API; on success call `upsertEvent(booking)` or `removeEvent(id)`; on failure call `failRequest(idOrToken, message)`. 4. Suppress the in-memory adapter by hiding or overriding your own listeners. --- ## Privacy The calendar module **does not touch the network itself**. Data arrives through `set*` / `upsertEvent` and leaves through `getEvents` / `exportAs`. No tiles, no autocomplete, no analytics. See the module docs for the full policy.