# 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.