/** * Calendar demo, Increment 1 — the pure model the rail-hosted surface * adds on top of `calendar.model.ts`: the four-way range view with its * keyboard shortcuts, the extended display settings (default duration, * name display, map link), the practice-hours time axis, the platform's * filter semantics (nothing ticked = everyone) and the split-button * badge count. No React, no i18n lookups. Every read of practice data * goes through the explicit `data: CalendarData` parameter. */ import type { AppointmentItem } from '../../components/appointment-card'; import type { CalendarResource } from '../../components/calendar'; import type { CalendarData } from './calendar.data'; import { type CalendarPatient, type CalendarWaitingEntry, type PatientChannelState, type PatientRef } from './calendar.types'; import { type CalendarRow, type DisplaySettings, type EditorDraft, type EditorPrefill, type WaitingQuickView } from './calendar.model'; export declare const WORKSPACE_TOOL: { readonly appointments: "appointments"; readonly editor: "appointment-editor"; readonly waitingList: "waiting-list"; readonly notes: "patient-notes"; readonly bookings: "online-bookings"; readonly findSlots: "find-slots"; }; /** One of the workspace rail's six tools. Notes requires patient context. */ export type CalendarWorkspaceToolId = (typeof WORKSPACE_TOOL)[keyof typeof WORKSPACE_TOOL]; /** Strip order — the order the rail registers the tools in, and the * order a host's `availableTools` is normalised back to. */ export declare const ALL_WORKSPACE_TOOLS: readonly CalendarWorkspaceToolId[]; /** * Which tools this host can back with real data, as a membership set. * * Appointments is forced in whatever the caller asked for: the rail * docks it as its resting panel, so leaving it out would leave an empty * dock rather than a smaller rail. An id the kit does not know is * ignored rather than thrown on — a host on a newer contract than the * kit it mounts must not crash the agenda. */ export declare function resolveAvailableTools(requested: readonly string[]): ReadonlySet; /** Whether `id` is one of the built-in rail tools. */ export declare function isWorkspaceToolId(id: string): id is CalendarWorkspaceToolId; /** Why a host tool was left off the rail. */ export type ExtraToolRejection = 'reserved' | 'duplicate' | 'empty'; /** The host tools the rail can carry, in the host's order. Built-in ids are * reserved even when withheld, a repeated id keeps its first tool, an empty * id is dropped, and nothing throws: a host mistake must not sink the page. */ export declare function resolveExtraTools(requested: readonly T[]): { tools: T[]; rejected: { id: string; reason: ExtraToolRejection; }[]; }; /** The platform's four ranges; "Working week" became the Show-weekends * item, so it is no longer a view of its own. */ export type WorkspaceViewId = 'day' | 'threeDays' | 'week' | 'month'; export declare const WORKSPACE_VIEWS: readonly WorkspaceViewId[]; /** CalendarView stem per range — composed with the column axis. */ export declare const WORKSPACE_VIEW_STEM: Record; /** Google-Calendar-style single-key shortcuts (X = 3 days, as GCal). */ export declare const VIEW_SHORTCUT_KEYS: Record; export declare const readWorkspaceViewId: (raw: unknown) => WorkspaceViewId | undefined; /** The range a bare key press selects, case-insensitive; undefined when * the key is not a shortcut. */ export declare function viewForShortcutKey(key: string): WorkspaceViewId | undefined; export declare function shortcutAllowedOn(target: EventTarget | null): boolean; /** The keydown shape the shortcut resolver reads — a subset of * KeyboardEvent so tests can pass plain objects. */ export interface ShortcutKeydown { key: string; altKey: boolean; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; repeat: boolean; defaultPrevented: boolean; target: EventTarget | null; } /** The range a page-level keydown switches to, or undefined when the * press must be left alone (modifier, auto-repeat, typing, an open * menu or dialog, already handled). */ export declare function shortcutViewForKeydown(event: ShortcutKeydown): WorkspaceViewId | undefined; /** Weekend visibility is a per-user choice now (platform hidden days): * off hides Saturday and Sunday in every range view. */ export declare function weekendHiddenDays(showWeekends: boolean): number[]; export type OperatorNameDisplay = 'full' | 'initials'; export type ChairNameDisplay = 'full' | 'short'; export interface WorkspaceDisplaySettings extends DisplaySettings { /** The per-user default appointment length (platform 30). */ defaultDurationMin: number; /** Column header treatment for people: dot + name, or initials disc. */ operatorNameDisplay: OperatorNameDisplay; /** Column header treatment for chairs: full name or the short code. */ chairNameDisplay: ChairNameDisplay; /** Show the Google Maps link in the appointment details. */ mapsLink: boolean; } export declare const DEFAULT_DURATION_RANGE: { readonly min: 5; readonly max: 240; readonly step: 5; }; export declare const DEFAULT_WORKSPACE_DISPLAY: WorkspaceDisplaySettings; /** Remembered Increment 1 display settings, validated per field over the * Increment 1 defaults (a Final State shape keeps every knob it knows). */ export declare function readWorkspaceDisplaySettings(raw: unknown): WorkspaceDisplaySettings | undefined; /** Earliest opening and latest closing across the practice's week — * the platform's `openingTime` / `closingTime` for the time axis. */ export declare function practiceDayWindow(data: CalendarData): { min: string; max: string; }; /** The axis for a cog window: `working` is the practice's own hours; * extended and full stay the Final State overrides. */ export declare function workspaceDayWindow(window: DisplaySettings['window'], data: CalendarData): { min: string; max: string; }; /** The ids a selection shows: the ticked ones, or all when none is. */ export declare function effectiveIds(selected: readonly string[], all: readonly string[]): string[]; /** Tick or untick `ids` in a selection (a row or a whole group). */ export declare function toggleSelectedIds(prev: readonly string[], ids: readonly string[], checked: boolean | 'indeterminate'): string[]; /** What the Reset chip counts: every ticked chair and operator. */ export declare function activeFilterCount(selectedChairIds: readonly string[], selectedOperatorIds: readonly string[]): number; /** * Disabled operators holding at least one appointment that STARTS inside * the visible range — the platform's carve-out * (`NewCalendar/Library.js`: `isActive(op) || hasAppointments(op)`). * Their column stands whatever the "Show disabled operators" switch * says, so hiding the operator can never hide an appointment. */ export declare function bookedDisabledOperatorIds(rows: readonly CalendarRow[], inactiveIds: readonly string[], range: { start: number; end: number; } | null): string[]; /** * The operators the grid may give a column to when NOTHING is ticked: * every active one, the disabled ones while the switch reveals them, and * any disabled operator the carve-out keeps. An explicit selection still * wins over all three (platform: `kept.filter(selected)`). */ export declare function columnableOperatorIds(args: { operators: readonly CalendarResource[]; inactiveIds: readonly string[]; showDisabled: boolean; keepIds: readonly string[]; }): string[]; /** The operators the filter list offers — disabled ones only once the * session switch reveals them (the carve-out is a column, not a row). */ export declare function listedOperators(operators: readonly CalendarResource[], inactiveIds: readonly string[], showDisabled: boolean): CalendarResource[]; /** Chair column label per the cog: the full name, or the fixture's short * code (the platform's "first two letters" setting). */ export declare function chairResourceTitle(chair: CalendarResource, mode: ChairNameDisplay): string; /** Operator column label: the plain name, or the name plus the muted * Disabled marker when the practice has disabled them. The kit Calendar * renders a resource header from its `title`, so the marker rides * there — and reaches the Avatar's accessible name under initials. */ export declare function operatorResourceTitle(operator: CalendarResource, isDisabled: boolean, disabledLabel: string): string; /** Whole minutes between two instants — the resize toast's figure. */ export declare function minutesBetween(start: Date | string, end: Date | string): number; /** Re-ends one appointment. The resize path writes through this twice — * once for the drag, once for the Undo that restores the old end — so * the two can never drift apart. */ export declare function rowsWithEnd(rows: readonly CalendarRow[], eventId: string, end: Date): CalendarRow[]; /** The platform's recurring-mode chooser (move, delete, save on edit). */ export type RecurringScope = 'this' | 'following' | 'all'; export declare const RECURRING_SCOPES: readonly RecurringScope[]; /** Literal map (not a template) so key extractors see every key. */ export declare const RECURRING_SCOPE_LABEL_KEY: Record; /** How the create form asks for the required documents' signature. */ export type RequiredDocsSignMode = 'sms' | 'email' | 'none'; /** What the editor reports beside the draft — the platform-only choices * the shared `EditorDraft` never needed. */ export interface EditorDraftExtras { recurringSaveMode?: RecurringScope; requiredDocsSignMode?: RequiredDocsSignMode; branchId?: string; } /** Delete confirmation choices — the recurring scope and, for a * price-list recurrence parent, the care-plan cleanup. */ export interface DeleteOptions { scope: RecurringScope; removeFromCarePlan?: boolean; } /** The occurrences a scope names, in series order — the event itself * for `this`, it and its later occurrences for `following`, the whole * series for `all`. A row outside a series only ever names itself. */ export declare function seriesScopeEventIds(rows: readonly CalendarRow[], eventId: string, scope: RecurringScope): string[]; /** Every timed appointment overlapping `[start, end)` — cancelled and * absent ones, tasks, washes and the row itself never count (platform * busy check). */ export declare function slotConflicts(rows: readonly CalendarRow[], slot: { start: Date; end: Date; chairId?: string; operatorId?: string; excludeEventId?: string; }): { chairBusy: boolean; operatorBusy: boolean; }; /** Devices the chosen cares need, deduplicated, in care order. */ export declare function devicesForCares(cares: readonly { careId?: string; }[] | undefined, devicesByCare: Readonly>): string[]; /** * Which sign / notify channels a patient can take a request on — as the * FOUR-state `PatientChannelState`, not a boolean. * * A boolean here was the bug: it folded "the record says the practice * holds no number" together with "we were never told", and every caller * then printed the first as the second ("No SMS-enabled number on * file") for a patient the host had simply supplied without a `phone`. * Callers now read the state: `channelReady` seeds a control ON, * `channelOfferable` decides whether to disable it at all, and * `channelReasonKey` gives a reason ONLY in the states that have one. */ export declare function signChannelStates(ref: PatientRef): { sms: PatientChannelState; email: PatientChannelState; }; /** The platform's default: SMS when the patient can take one, else * email, else nothing. "Can take one" is `ready` alone — a channel we * know nothing about is not a channel to send on by default. */ export declare function defaultSignMode(ref: PatientRef): RequiredDocsSignMode; export { patientAddressLabel, patientAddressLine, patientMapsUrl, } from '../../components/_shared/appointment-format'; /** A CREATE mode armed by a deep link (`createParameters`) or the * Backlog's Schedule: the next empty-slot click opens the editor with * this context already filled in (platform `_switchMode('create')`). */ export interface CreateModeRequest { tick: number; prefill: EditorPrefill; /** Named in the banner when the context carries a patient. */ patientName?: string; } /** Legible ink for a mode banner tinted with an appointment colour. The * Google palette ships no text colour, so the pair is settled by the * kit's own contrast maths rather than by eye. */ export declare function bannerTextColor(backgroundHex?: string): string | undefined; /** A Move… or Copy… armed from the details dialog: the next empty-slot * click places the source appointment there. */ export interface PlacementRequest { kind: 'move' | 'copy'; eventId: string; /** Copy only — carry the linked treatments along (platform's two copies). */ withTreatments?: boolean; /** Armed from a waiting-list Reschedule — the entry leaves the queue * once the move lands (platform parity). */ waitingEntryId?: string; } /** The platform's "Copia & incolla": the same appointment on a new slot * with a fresh id — never a series member, treatments only on request. * An all-day source stays all-day on the target date. */ export declare function copyRowAt(source: CalendarRow, target: { start: Date; resourceId?: string; axisKind: DisplaySettings['resourceKind']; }, id: string, withTreatments: boolean): CalendarRow; /** Apply an edit saved with "this and following" / "all" to the other * occurrences: each keeps its own day shifted by the same delta as the * edited one, and takes the edited duration, allocation, colour, status, * type and description. */ export declare function applySeriesEdit(rows: readonly CalendarRow[], primaryPrev: CalendarRow, draft: EditorDraft, siblingIds: readonly string[], data: CalendarData): CalendarRow[]; /** What the post-save toast needs to decide whether the saved * appointment is on screen: its day and its allocation. */ export interface SaveNotice { tick: number; date: Date; chairId?: string; operatorId?: string; } /** Why a saved appointment is not on the grid right now — empty when it * is visible. The "Go to" action undoes exactly these. */ export declare function hiddenReasons(notice: Pick, view: { visibleRange: { start: number; end: number; } | null; hiddenDays: readonly number[]; selectedChairIds: readonly string[]; selectedOperatorIds: readonly string[]; }): Array<'range' | 'weekend' | 'chair' | 'operator'>; /** * The platform DASHBOARD widget's window (`upcomingAppointmentsAction`): * NOW → `forwardDays` later at 23:59:59.999, cancelled excluded, date * ascending, no row limit. `backDays` is 0 at rest — the kit timeline's * presets and its edge loaders widen the derived window in place of a * fetch, so the panel can look further than the platform's seven days. */ export interface UpcomingWindow { /** Days of history the panel has been widened to; 0 starts at NOW. */ backDays: number; /** Days ahead — the platform's seven at rest. */ forwardDays: number; } /** Platform default: now to the end of the seventh day. */ export declare const UPCOMING_FORWARD_DAYS = 7; /** How far one widening step reaches at either edge. */ export declare const UPCOMING_STEP_DAYS = 7; /** Where the widening stops — the demo's own ceiling, not the platform's. */ export declare const UPCOMING_MAX_DAYS = 180; export declare const DEFAULT_UPCOMING_WINDOW: UpcomingWindow; /** * Epoch bounds of a window, inclusive of its closing second. * * The start always lands on a day boundary — midnight of * `nowIso - backDays` — never at the exact instant of `now`. At rest * (`backDays: 0`) that means "the beginning of today", not "this second": * a row that has already started (waiting room, in the chair) stays in * the window instead of disappearing the moment it begins. Widening * (`widenUpcomingWindow('earlier', …)`) reaches further back a whole day * at a time from that same floor. */ export declare function upcomingWindowBounds(nowIso: string, window: UpcomingWindow): { startMs: number; endMs: number; }; /** One widening step at either edge, capped at the ceiling. */ export declare function widenUpcomingWindow(window: UpcomingWindow, edge: 'earlier' | 'later'): UpcomingWindow; /** Widen just far enough to reach `targetIso` — the timeline's presets and * its jump-to-date land on days the default seven never covered. */ export declare function upcomingWindowCovering(window: UpcomingWindow, nowIso: string, targetIso: string): UpcomingWindow; /** * The rows the widget lists: appointments starting inside the window, * cancelled excluded, date ascending. Background washes and the composing * draft never qualify; tasks are already out of the Increment 1 feed. */ export declare function upcomingAppointmentRows(rows: CalendarRow[], bounds: { startMs: number; endMs: number; }): CalendarRow[]; /** * One agenda row as the kit timeline's item — identity, allocation and * care lines; the row id stays the event id so a click maps straight * back onto the grid. * * The card prints its own "no patient assigned" whenever `patient` is * absent, so absence here has to mean exactly that: the appointment * carries NO patient id. A row that carries one but whose record the * host could not resolve still names its patient — from the record, from * the title the row already carries, and only then from * `unresolvedPatientLabel`, which must say the record was not loaded and * never that nobody is booked. */ export declare function upcomingAppointmentItem(row: CalendarRow, privacy: boolean, data: CalendarData, /** Localised name-of-last-resort for an unresolvable patient id. */ unresolvedPatientLabel: string, /** Localised stand-in for a row whose details the host is still * fetching (`detailsPending`). Such a row carries no patient id and * no title, so without it the feed would print a nameless line. */ pendingDetailsLabel?: string): AppointmentItem; /** * True when a row still needs completing before it is a real, workable * appointment: no patient record, no operator, or — only where the * practice actually has chairs — no chair. Tasks never qualify (the * caller already drops them from `rows`, but a defensive check costs * nothing). Origin (online vs. desk-created) is irrelevant: a gap is a * gap regardless of how the appointment was created. */ export declare function appointmentNeedsCompletion(row: CalendarRow, data: CalendarData): boolean; /** * The backlog queue: every row in `rows` that still needs a patient, an * operator or a chair. Mirrors the platform's alert-strip query in * spirit, but is no longer scoped to online bookings or to a 30-day * window — the caller already hands in only the rows in the loaded * range, and a desk-created gap is exactly as real as an online one. */ export declare function onlineBookingsToCheck(rows: readonly CalendarRow[], data: CalendarData): CalendarRow[]; /** * One row of the backlog queue (formerly the platform's "Prenotazioni * online" page, which Increment 1 hosts in the rail instead of sending * the user to a separate page). Columns mirror it: when it was booked * (a desk-created slot carries no `bookedAt` and groups under its own * appointment date instead), what was asked for, whether it was paid * online, and what is still missing before it is a real appointment. */ export interface BookingRowView { row: CalendarRow; eventId: string; /** The appointment's own start. */ start: Date; /** Platform `createdAt` — "del " under the appointment date. * Null for a desk-created slot, which has no booking record. */ bookedAt: Date | null; /** The booking reason, or the platform's "Non specificato". */ reason: string | null; chairTitle: string | null; /** Stripe amount when the patient paid at booking. */ paidOnline: number | null; needsPatient: boolean; needsOperator: boolean; /** True only where the practice has chairs at all — see * `appointmentNeedsCompletion`. */ needsChair: boolean; /** Badge fact, not a filter: origin no longer decides membership, but * the card still says where the appointment came from. */ bookedOnline: boolean; /** Booked after the user last opened the page — the "(Nuovo)" marker. */ isNew: boolean; } export declare function bookingRowView(row: CalendarRow, data: CalendarData, lastSeenMs?: number | null, createdAtIso?: string): BookingRowView; /** The three completion gaps a backlog card can show, in filter order. * Chair is only ever offered when the practice has chair resources. */ export type BacklogGap = 'patient' | 'operator' | 'chair'; /** All three switches on by default — the uncontrolled starting point * for a host that does not persist the filter state itself. */ export declare const DEFAULT_BACKLOG_FILTERS: Readonly>; /** Applies the header's three switches to an already-built view list. A * view is kept when it carries at least one gap left switched on — an * appointment missing only a patient still shows with "Senza operatore" * and "Senza poltrona" off, since it still needs a patient. */ export declare function filterBacklogViews(views: readonly BookingRowView[], filters: Readonly>): BookingRowView[]; /** The nine advanced inputs, all optional. Empty = the input is unset; * `operatorId` also accepts `NONE`, the platform's "no operator". */ export interface WaitingListFilters { patientId: string; patientAssigned: '' | 'yes' | 'no'; operatorId: string; appointmentScheduled: '' | 'yes' | 'no'; appointmentFrom?: Date; appointmentTo?: Date; waitingFrom?: Date; waitingTo?: Date; description: string; } export declare const EMPTY_WAITING_FILTERS: WaitingListFilters; /** How many of the nine inputs carry a value — the toggle's badge. */ export declare function waitingFiltersActiveCount(filters: WaitingListFilters): number; /** * One waiting row resolved the way the platform's serializer resolves * it: the entry's OWN patient, the linked appointment with its patient, * care line and colour, and the operator default. `noConnection` is the * platform's "Nessun collegamento" test — none of the four is present. */ export interface WaitingListRowView { entry: CalendarWaitingEntry; /** The entry's OWN patient, id and record together. `assigned` is what * decides whether this entry has a patient — the record may simply * not have loaded. */ patientRef: PatientRef; /** Platform `patientId` / `patientName` / `patientPhoneNumber` — the * record alone, when the host could load it. */ patient?: CalendarPatient; /** The linked appointment, live from the grid's rows. */ appointment?: CalendarRow; /** Platform `appointmentPatientId` — the linked appointment's patient, * id and record together. */ appointmentPatientRef: PatientRef; /** The linked appointment's patient record, when it loaded. */ appointmentPatient?: CalendarPatient; /** Platform `operatorName`. */ operatorTitle?: string; /** Platform `appointmentName` — the linked appointment's care line. */ appointmentName?: string; /** The row's one photo: the resolved patient record's, else the * entry's own `patientAvatarUrl`. Absent = initials. */ avatarSrc?: string; /** Someone is booked on this row — the entry's own patient or the * linked appointment's. Keyed on the IDS: a record we could not load * is still a patient. */ patientAssigned: boolean; noConnection: boolean; } export declare function waitingListRowView(entry: CalendarWaitingEntry, rows: readonly CalendarRow[], data: CalendarData): WaitingListRowView; /** * The platform's advanced predicates, in its own order. A patient * matches the entry's OWN patient or the linked appointment's; the * operator and the description only ever read the ENTRY's own fields. */ export declare function waitingEntryMatchesFilters(view: WaitingListRowView, filters: WaitingListFilters, data: CalendarData): boolean; /** The quick view first, then the advanced predicates — the platform's * own order (`_filterForAdvancedSearch` re-runs the view). */ export declare function filterWaitingEntries(entries: readonly CalendarWaitingEntry[], rows: readonly CalendarRow[], quickView: WaitingQuickView, filters: WaitingListFilters, data: CalendarData): WaitingListRowView[]; /** What an entry is FOR. The domain allows exactly one connection, so * the test is a precedence, and an entry never lands in two groups. */ export type WaitingEntryKind = 'linked' | 'patient' | 'memo'; /** Render order: bring forward, first slot, free-text reminders. */ export declare const WAITING_ENTRY_KINDS: readonly WaitingEntryKind[]; /** Keyed on the IDS, never on what resolved: an appointment off the * loaded range is still a link, an unloaded patient still a patient. */ export declare function waitingEntryKind(entry: CalendarWaitingEntry): WaitingEntryKind; /** The non-empty groups, in render order, rows in queue order. */ export declare function groupWaitingRows(views: readonly WaitingListRowView[]): { kind: WaitingEntryKind; rows: WaitingListRowView[]; }[]; /** How many entries each quick view holds, from the rows in hand. */ export declare function waitingQuickViewCounts(entries: readonly CalendarWaitingEntry[], data: CalendarData): Record; /** The number WhatsApp may be handed, `''` when there is none. The host's * own `whatsappPhoneNumber` wins; the story flag reads the record. */ export declare function waitingWhatsAppNumber(view: WaitingListRowView): string; /** A host's read of the queue, so the panel can show a first load. */ export interface CalendarWaitingListState { status: 'loading' | 'ready' | 'error'; onRetry?: () => void; } //# sourceMappingURL=calendar-workspace.model.d.ts.map