Manual

Manual · The book

Calendar

Appointments you can drag — month, week, day, agenda, or a board of rooms.

A list in another shape

A calendar is an object you put on a form, and underneath it is the list you already know: it shows a table or a relationship, its columns are paths (customer.name works), it writes through the same funnel as everything else, and it goes stale and re-reads for the same reasons. What is new is the arithmetic of overlapping appointments and the dragging.

Point it at a table, name the field an appointment starts in, and it draws. Name an end field and appointments have length; name a title, a kind, a colour and a lane and it uses them. All-day is read off the field’s type — a Date is a day, a Timestamp is a time — so there is nothing to tick.

Office hours

Narrow Office hours inside the hours the calendar draws and everything outside them is drawn quieter — the weekend with it, because it is the same statement about which time is ordinary. Left equal to the drawn hours they tint nothing, which is why there is no switch beside them.

Five views, and one of them is a board

Month · Week · Day · Agenda · Board. You choose which of them the window offers; with one, the switcher disappears. The board is the one that is not about days: it shows a single day with one column per lane — rooms, vans, people — and the lane comes from a field or from a path into a related table (room.name), in which case a room with nothing booked still gets its column. That empty column is usually the one you opened the board to find.

The whole loop, with no script at all

Refusing a move belongs on the table. The calendar writes through DatabaseSession like the form, the import and every script — so a rule you wrote once in tables/<t>/onValidate.js applies to the mouse too: the appointment springs back and shows the sentence you wrote. There is deliberately no veto event of its own, because a second place to forbid a move would be one only the mouse knows about.

No recurring appointments — on purpose

One row is one appointment. A repeat rule inside a record would be a format that no list, no report, no export and no query understands, and the first person to ask “how many visits in March?” would get a wrong answer from their own data. Write the rows you mean — a script that makes twelve of them is four lines.

What a script can ask, and tell

form.planner.datethe day it is standing on; write it and the calendar goes there
form.planner.view"month", "week", "day", "agenda", "board"
form.planner.appointmentsthe records on screen, in time order
form.planner.range{ from, to } of the stretch being shown
form.planner.valuethe selected appointment, or null
form.planner.show(query)show what this query finds — the visible stretch is added to it, so flipping the month keeps the filter
form.planner.freeSlots(minutes[, lane])every gap on screen at least that long, as [{ from, to }] — and on a board, one room’s
form.planner.newAppointment(from[, to])as if it had been drawn: the event fires, the window opens
form.planner.select(record)go to it, mark it, scroll it into view
form.planner.scrollTo(when)that time, near the top of the window
form.planner.calendarFile([records])an .ics file: .saveTo(path) writes it, .open() hands it to the Mac’s own calendar. Without records, what is on screen
form.planner.markAsProblem(record, "…")a red edge and that sentence as a tooltip; null takes it back
form.planner.refresh()the same appointments, read again
// "When could we all meet?" — the overlapping arithmetic, run backwards.
const gaps = form.planner.freeSlots(60, "Bridge");
if (gaps.length === 0) {
    messages.alert("Nothing free of an hour or more in what is on screen.");
    return;
}
messages.alert("Free on the bridge: " + gaps[0].from.toLocaleString());

Drag something in from a list

A list of what has no date yet beside a calendar, and you drag a row into Tuesday morning: that is the planning gesture, and it needs one script. Every list’s rows can be dragged out — there is nothing to switch on — and a calendar accepts one only when it has an onDropped script, because a drop that is accepted and then does nothing reads as a fault in the thing you dropped.

The calendar writes nothing itself. What was dropped may be an order, a repair or a person; which of its fields means “when”, or whether a second record should be made pointing at it, is your data model’s question and not the object’s — the same line the lane is on.

// forms/Planner/planner.onDropped.js
// A customer, let go over a free morning.
const visit = database.appointments.create();
visit.subject  = "Visit — " + event.record.name;
visit.customer = event.record;
visit.startsAt = event.from;
visit.endsAt   = event.to;
visit.save();
form.planner.select(visit);

On paper, and in somebody else’s calendar

Tick Print among the calendar’s buttons and it opens the report manager with the appointments that are on screen — the same road a list takes, so your own report shapes are the ones offered and there is no second printing engine to look different. And form.planner.calendarFile([…]).open() writes the appointment as an .ics and hands it to the Mac: whoever gets it presses it once and it is in their calendar, whatever they use. The format’s traps — CRLF, folding at seventy-five octets, an escaped comma, times in UTC, an all-day end that is the day after — are ours, not yours.

The events

onNewAppointmentdrawn or double-clicked; event.record is there to be filled in, event.from, event.to, event.lane. Refusing drops it.
onAppointmentMovedafter a move the table allowed; event.change is "moved" or "resized", with event.previousFrom and event.previousTo
onAppointmentClickone click, event.record
onAppointmentDoubleClickreplaces opening the window, rather than running beside it
onDateRangeChangedthe view or the stretch changed: event.from, event.to, event.view
onDroppeda record was dragged in from a list: event.record, event.from, event.to, event.lane. Without this script the calendar refuses the drag
onSelectionChangethe selected appointment changed

Colour cascades. The appointment’s own colour field beats the colour you gave its kind, which beats one worked out from the value itself — so a calendar nobody has configured is still readable, and the same kind keeps the same colour after a restart.

Somebody else moves one

In a Client/Server pair the calendar re-reads when another desk writes, and it shows the journey rather than the result: the chip slides to its new time with a ring that fades as it lands. It costs nothing on the connection — both pictures were already in memory — and a move you made yourself is not animated, because you were there.