The availability bar showed the guests it had free/busy for and quietly left out the ones it did not. In one row that is survivable. As a grid it would be a lie: a row with nothing in it reads as a diary with nothing in it, and "we cannot see this person's calendar" is the one thing that must not look like "this person is free". So everyone the event concerns now gets a row, and the ones with no free/busy to read are drawn hatched rather than empty, with a line under the grid saying how many and why. You get a row too, first. Scheduling around the other people and not around yourself is how two things end up at the same time, and the organiser was the one calendar the panel never showed. Your own row is never unknown. Where the directory does not list you under the address your identity sends from -- an alias, a login that differs from the address -- the account is still yours to read, and Stalwart answers for it under the account's own id. The window steps backwards and forwards a screenful at a time without touching the event, which is the "movable forwards & backwards" the report asks for, and offers its way back when you have wandered off. And the bars are somewhere to put the event rather than only something to read: the pointer shows the half hour it is over, and a click moves the event there keeping its length. Clicking while stepped away moves the event to where you clicked and returns the view to it, so it lands where you were looking instead of jumping. Free/busy is answered per principal, and only the server's own accounts are principals. Somebody at another domain has none to read -- which is not a gap to be closed, it is what the protocol can see -- so the grid says so rather than drawing them blank. Closes #172
96 lines
4.0 KiB
TypeScript
96 lines
4.0 KiB
TypeScript
import { DAY_MS } from "@/lib/dates";
|
|
|
|
/**
|
|
* The span an availability bar covers, and the marks along it.
|
|
*
|
|
* The bar used to be a day wide whatever it was showing: it began at midnight
|
|
* on the event's start day and stopped 24 hours later, so an event running over
|
|
* two days showed availability for the first of them and gave no sign that
|
|
* there was more. It also carried no marks at all, which left "is this the
|
|
* whole day or only working hours" unanswerable without dragging the event
|
|
* around to see where its own outline moved. That is issue #172, parts 1 and 2.
|
|
*
|
|
* Whole days, always: a bar that started at the event's own start time would
|
|
* move under the reader every time they adjusted it, and "busy from about a
|
|
* third of the way along" is not a time anybody can read.
|
|
*/
|
|
export interface AvailabilityWindow {
|
|
/** Midnight at the start of the first day shown. */
|
|
start: Date;
|
|
/** Midnight at the end of the last day shown. */
|
|
end: Date;
|
|
/** Milliseconds between the two, which a DST change makes not a multiple of a day. */
|
|
span: number;
|
|
/** Days actually shown. */
|
|
days: number;
|
|
/**
|
|
* Marks along the bar. `at` is a fraction of the span, so a caller positions
|
|
* one with a percentage and never does date arithmetic of its own. Only
|
|
* `major` marks are worth a label; the rest are there to read a block against.
|
|
*/
|
|
ticks: { at: number; time: Date; major: boolean }[];
|
|
/** Whether marks fall on hours or on days, which decides how to label them. */
|
|
scale: "hours" | "days";
|
|
/**
|
|
* Days the event covers that the bar does not. An event long enough to need
|
|
* this is not one anybody is checking for a free slot, and drawing a month at
|
|
* eight pixels a day would say nothing; saying how much was left out is more
|
|
* use than showing it.
|
|
*/
|
|
daysHidden: number;
|
|
}
|
|
|
|
/** Midnight starting the day `d` falls in, in local time. */
|
|
function startOfDay(d: Date): Date {
|
|
const out = new Date(d);
|
|
out.setHours(0, 0, 0, 0);
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* `n` days on from `d`, by the calendar rather than by arithmetic: a day is 23
|
|
* or 25 hours twice a year, and adding 24 of them lands an hour off.
|
|
*/
|
|
function addDays(d: Date, n: number): Date {
|
|
const out = new Date(d);
|
|
out.setDate(out.getDate() + n);
|
|
out.setHours(0, 0, 0, 0);
|
|
return out;
|
|
}
|
|
|
|
/** How far apart the marks go, in hours, and which of them get a label. */
|
|
function spacing(days: number): { every: number; label: number } {
|
|
if (days <= 1) return { every: 3, label: 6 };
|
|
if (days <= 2) return { every: 6, label: 12 };
|
|
return { every: 24, label: 24 };
|
|
}
|
|
|
|
export function availabilityWindow(start: Date, end: Date, opts: { maxDays?: number; offsetDays?: number } = {}): AvailabilityWindow {
|
|
const maxDays = opts.maxDays ?? 7;
|
|
/*
|
|
* Days moved from where the event sits, for looking around it without
|
|
* changing it. The whole window slides rather than growing: keeping the span
|
|
* fixed means what you compare when you step forward is the same width as
|
|
* what you were looking at, which is the point of stepping.
|
|
*/
|
|
const from = addDays(startOfDay(start), opts.offsetDays ?? 0);
|
|
// The last day is the one the event ends *on*. An event ending exactly at
|
|
// midnight ends on the day before, not at the start of a day it never
|
|
// touches -- that is the whole of what all-day events do.
|
|
const lastDay = addDays(startOfDay(new Date(Math.max(end.getTime() - 1, start.getTime()))), opts.offsetDays ?? 0);
|
|
const total = Math.max(1, Math.round((lastDay.getTime() - from.getTime()) / DAY_MS) + 1);
|
|
const days = Math.min(total, maxDays);
|
|
const to = addDays(from, days);
|
|
const span = to.getTime() - from.getTime();
|
|
|
|
const { every, label } = spacing(days);
|
|
const ticks: AvailabilityWindow["ticks"] = [];
|
|
for (let hour = 0; ; hour += every) {
|
|
const time = new Date(from.getTime() + hour * 3600_000);
|
|
if (time.getTime() >= to.getTime()) break;
|
|
ticks.push({ at: (time.getTime() - from.getTime()) / span, time, major: hour % label === 0 });
|
|
}
|
|
|
|
return { start: from, end: to, span, days, ticks, scale: days <= 2 ? "hours" : "days", daysHidden: total - days };
|
|
}
|