payload-reserve 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,7 +19,7 @@ Designed for salons, clinics, hotels, restaurants, event venues, and any busines
19
19
  - **Resource Owner Multi-Tenancy** — Opt-in `resourceOwnerMode` wires ownership access control so each resource owner (host) sees only their own listings and reservations
20
20
  - **Configurable Status Machine** — Define your own statuses, transitions, blocking states, terminal states, and the `confirmStatus`/`cancelStatus` that drive the confirm/cancel hooks and cancellation policy
21
21
  - **Double-Booking Prevention** — Server-side conflict detection that enforces both bookings' buffer times and checks each resource only for its own item window; respects capacity modes
22
- - **Business Timezone** — Set a plugin-level `timezone` (IANA, default `'UTC'`) so schedules, day boundaries, and the admin calendar resolve in your business's timezone regardless of server location
22
+ - **Business Timezone** — Set a plugin-level `timezone` (IANA, default `'UTC'`) so schedules, day boundaries, and the admin calendar resolve in your business's timezone regardless of server location — with optional **per-tenant** zones in `multiTenant` mode
23
23
  - **Auto End Time** — Calculates `endTime` from `startTime + service.duration` automatically
24
24
  - **Three Duration Types** — `fixed` (service duration), `flexible` (customer-specified end), and `full-day` bookings
25
25
  - **Multi-Resource Bookings** — Single reservation that spans multiple resources simultaneously via the `items` array
@@ -253,6 +253,21 @@ payloadReserve({
253
253
 
254
254
  Without this, day resolution mixes server-local and UTC semantics — on non-UTC servers the slots API can resolve the wrong calendar day. With `timezone` set, all server-side day math runs via the built-in `Intl` API (no extra dependency). `'UTC'` on a UTC server is identical to the previous behaviour. The configured timezone is exposed to admin components via `config.admin.custom.reservationTimezone`.
255
255
 
256
+ #### Per-tenant timezones (`multiTenant`)
257
+
258
+ When tenant scoping is active, the admin Calendar, Availability grid, and Dashboard widget resolve day-boundaries in the **selected tenant's** zone instead of one global zone — so a tenant in `America/New_York` and a tenant in `Europe/Paris` each see their own local days. Point `multiTenant.timezoneField` at the IANA timezone field on your tenant document:
259
+
260
+ ```typescript
261
+ payloadReserve({
262
+ timezone: 'UTC', // global default / fallback
263
+ multiTenant: {
264
+ timezoneField: 'timezone', // field on the tenant doc (default: 'timezone')
265
+ },
266
+ })
267
+ ```
268
+
269
+ Resolution precedence is `tenant.<timezoneField> → global timezone → 'UTC'`; a tenant with no (or an invalid) timezone value transparently falls back to the global default. The zone is resolved server-side from the tenant cookie — the client calendar reads it from `GET /api/reserve/effective-timezone`. This is purely additive: plain single-tenant installs (no tenant relationship / no tenant cookie) keep the global zone with no extra DB read.
270
+
256
271
  ### Collection Overrides
257
272
 
258
273
  Customize any generated collection without forking the plugin via `collectionOverrides`. Each entry is a `Partial<CollectionConfig>` (minus `fields`/`slug`) plus a `fields` function that receives the plugin's default fields:
@@ -33,10 +33,43 @@ export const AvailabilityOverview = ()=>{
33
33
  'pending',
34
34
  'confirmed'
35
35
  ];
36
- const reservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
36
+ // In multiTenant mode the business timezone is the SELECTED tenant's zone,
37
+ // resolved server-side from the tenant cookie; fall back to the static global
38
+ // zone until it resolves and for plain installs.
39
+ const staticReservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
37
40
  const resourcesTenantParams = useTenantFilter(slugs?.resources ?? 'resources');
38
41
  const schedulesTenantParams = useTenantFilter(slugs?.schedules ?? 'schedules');
39
42
  const reservationsTenantParams = useTenantFilter(slugs?.reservations ?? 'reservations');
43
+ const [effectiveTimezone, setEffectiveTimezone] = useState(null);
44
+ const reservationTimezone = effectiveTimezone ?? staticReservationTimezone;
45
+ const tenantKey = JSON.stringify(reservationsTenantParams);
46
+ useEffect(()=>{
47
+ let cancelled = false;
48
+ const apiBase = `${config.serverURL ?? ''}${config.routes.api}`;
49
+ void (async ()=>{
50
+ try {
51
+ const res = await fetch(`${apiBase}/reserve/effective-timezone`, {
52
+ credentials: 'same-origin'
53
+ });
54
+ if (!res.ok) {
55
+ return;
56
+ }
57
+ const json = await res.json();
58
+ if (!cancelled && typeof json?.timeZone === 'string') {
59
+ setEffectiveTimezone(json.timeZone);
60
+ }
61
+ } catch {
62
+ // keep the static fallback
63
+ }
64
+ })();
65
+ return ()=>{
66
+ cancelled = true;
67
+ };
68
+ }, [
69
+ config.serverURL,
70
+ config.routes.api,
71
+ tenantKey
72
+ ]);
40
73
  const DAY_NAMES = useMemo(()=>[
41
74
  t('reservation:dayShortSun'),
42
75
  t('reservation:dayShortMon'),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/AvailabilityOverview/index.tsx"],"sourcesContent":["'use client'\nimport type { AdminViewServerProps } from 'payload'\n\nimport { useConfig, useTranslation } from '@payloadcms/ui'\nimport { Fragment, useCallback, useEffect, useMemo, useState } from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\n\nimport { getDayKeyInTimezone, getDayOfWeekFromDayKey } from '../../utilities/timezoneUtils.js'\nimport { useTenantFilter } from '../../utilities/useTenantFilter.js'\nimport styles from './AvailabilityOverview.module.css'\n\ntype Resource = {\n active?: boolean\n capacityMode?: 'per-guest' | 'per-reservation'\n id: string\n name: string\n quantity?: number\n}\n\ntype Schedule = {\n active?: boolean\n exceptions?: Array<{ date: string; endDate?: string; reason?: string }>\n id: string\n manualSlots?: Array<{ date: string; endTime: string; startTime: string }>\n recurringSlots?: Array<{ day: string; endTime: string; startTime: string }>\n resource: { id: string } | string\n scheduleType: 'manual' | 'recurring'\n}\n\ntype Reservation = {\n endTime?: string\n id: string\n resource: { id: string } | string\n startTime: string\n status: string\n}\n\nconst DAY_MAP: Record<string, number> = {\n fri: 5,\n mon: 1,\n sat: 6,\n sun: 0,\n thu: 4,\n tue: 2,\n wed: 3,\n}\n\n/** Return the CSS class for a capacity badge based on utilization ratio. */\nfunction capacityClass(booked: number, total: number): string {\n if (booked >= total) {return styles.slotCapacityFull}\n if (booked / total >= 0.5) {return styles.slotCapacityMid}\n return styles.slotCapacityLow\n}\n\nexport const AvailabilityOverview: React.FC<AdminViewServerProps> = () => {\n const { config } = useConfig()\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n const slugs = config.admin?.custom?.reservationSlugs\n const statusMachine = config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? ['pending', 'confirmed']\n const reservationTimezone: string = config.admin?.custom?.reservationTimezone ?? 'UTC'\n\n const resourcesTenantParams = useTenantFilter(slugs?.resources ?? 'resources')\n const schedulesTenantParams = useTenantFilter(slugs?.schedules ?? 'schedules')\n const reservationsTenantParams = useTenantFilter(slugs?.reservations ?? 'reservations')\n\n const DAY_NAMES = useMemo(\n () => [\n t('reservation:dayShortSun'),\n t('reservation:dayShortMon'),\n t('reservation:dayShortTue'),\n t('reservation:dayShortWed'),\n t('reservation:dayShortThu'),\n t('reservation:dayShortFri'),\n t('reservation:dayShortSat'),\n ],\n [t],\n )\n\n const [weekStart, setWeekStart] = useState(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n return d\n })\n\n const [resources, setResources] = useState<Resource[]>([])\n const [schedules, setSchedules] = useState<Schedule[]>([])\n const [reservations, setReservations] = useState<Reservation[]>([])\n const [loading, setLoading] = useState(true)\n\n // Cells are browser-local midnights by design; every key derived from them\n // (day keys, weekday matching) is computed in the business timezone.\n const weekDays = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + i)\n return d\n })\n }, [weekStart])\n\n const weekEnd = useMemo(() => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + 6)\n d.setHours(23, 59, 59, 999)\n return d\n }, [weekStart])\n\n useEffect(() => {\n if (!slugs) {return}\n\n // Ignore a slow earlier fetch if the week changed before it resolved (D5)\n let stale = false\n\n const fetchData = async () => {\n setLoading(true)\n const apiBase = `${config.serverURL ?? ''}${config.routes.api}`\n\n // Build a query that fetches only blocking-status reservations so the\n // component doesn't need to filter client-side. The `in` operator on\n // Payload's REST API accepts a comma-separated list.\n const blockingIn = blockingStatuses.join(',')\n\n try {\n const resourcesParams = new URLSearchParams({\n limit: '100',\n 'where[active][equals]': 'true',\n ...resourcesTenantParams,\n })\n const schedulesParams = new URLSearchParams({\n limit: '1000',\n 'where[active][equals]': 'true',\n ...schedulesTenantParams,\n })\n const reservationsParams = new URLSearchParams({\n depth: '0',\n limit: '2000',\n 'where[startTime][greater_than_equal]': weekStart.toISOString(),\n 'where[startTime][less_than_equal]': weekEnd.toISOString(),\n 'where[status][in]': blockingIn,\n ...reservationsTenantParams,\n })\n const [resourcesRes, schedulesRes, reservationsRes] = await Promise.all([\n fetch(`${apiBase}/${slugs.resources}?${resourcesParams}`),\n fetch(`${apiBase}/${slugs.schedules}?${schedulesParams}`),\n fetch(`${apiBase}/${slugs.reservations}?${reservationsParams}`),\n ])\n\n const [rData, sData, resData] = await Promise.all([\n resourcesRes.json(),\n schedulesRes.json(),\n reservationsRes.json(),\n ])\n\n if (stale) {return}\n setResources(rData.docs ?? [])\n setSchedules(sData.docs ?? [])\n setReservations(resData.docs ?? [])\n } catch {\n if (stale) {return}\n setResources([])\n setSchedules([])\n setReservations([])\n }\n if (!stale) {setLoading(false)}\n }\n\n void fetchData()\n return () => {\n stale = true\n }\n // blockingStatuses is derived from config which is stable; stringify to\n // avoid object-reference churn causing infinite loops.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [weekStart, weekEnd, config.routes.api, config.serverURL, slugs, blockingStatuses.join(','), resourcesTenantParams, schedulesTenantParams, reservationsTenantParams])\n\n const navigateWeek = useCallback((direction: -1 | 1) => {\n setWeekStart((prev) => {\n const next = new Date(prev)\n next.setDate(next.getDate() + 7 * direction)\n return next\n })\n }, [])\n\n const goToThisWeek = useCallback(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n setWeekStart(d)\n }, [])\n\n const getResourceId = (r: { id: string } | string) =>\n typeof r === 'object' ? r.id : r\n\n const getSlotsForResourceDay = (resourceId: string, day: Date) => {\n const resourceSchedules = schedules.filter(\n (s) => getResourceId(s.resource) === resourceId,\n )\n const dateStr = getDayKeyInTimezone(day, reservationTimezone)\n const dayOfWeek = getDayOfWeekFromDayKey(dateStr)\n\n const slots: Array<{ label: string; type: 'available' | 'exception' }> = []\n\n for (const schedule of resourceSchedules) {\n // Check for exceptions — match the full [date, endDate] range inclusively\n // (review D4), keyed in the business timezone, like the server does.\n const exception = schedule.exceptions?.find((e) => {\n const start = getDayKeyInTimezone(new Date(e.date), reservationTimezone)\n const end = e.endDate ? getDayKeyInTimezone(new Date(e.endDate), reservationTimezone) : start\n return dateStr >= start && dateStr <= end\n })\n\n if (exception) {\n slots.push({\n type: 'exception',\n label: exception.reason || t('reservation:availabilityUnavailable'),\n })\n continue\n }\n\n if (schedule.scheduleType === 'recurring') {\n for (const slot of schedule.recurringSlots ?? []) {\n if (slot.day === dayOfWeek) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n } else if (schedule.scheduleType === 'manual') {\n for (const slot of schedule.manualSlots ?? []) {\n const slotDate = getDayKeyInTimezone(new Date(slot.date), reservationTimezone)\n if (slotDate === dateStr) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n }\n }\n\n return slots\n }\n\n /** Returns all blocking-status reservations for a resource on a given day. */\n const getBookingsForResourceDay = (resourceId: string, day: Date) => {\n const dayKey = getDayKeyInTimezone(day, reservationTimezone)\n return reservations.filter((r) => {\n return (\n getResourceId(r.resource) === resourceId &&\n getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === dayKey\n )\n })\n }\n\n if (!slugs) {\n return <div className={styles.noResources}>{t('reservation:availabilityNotConfigured')}</div>\n }\n\n if (loading) {\n return <div className={styles.loading}>{t('reservation:availabilityLoading')}</div>\n }\n\n const weekLabel = `${weekDays[0].toLocaleDateString([], { day: 'numeric', month: 'short', timeZone: reservationTimezone })} - ${weekDays[6].toLocaleDateString([], { day: 'numeric', month: 'short', timeZone: reservationTimezone, year: 'numeric' })}`\n\n const gridColumns = `150px repeat(7, 1fr)`\n\n return (\n <div className={styles.wrapper}>\n <h2 className={styles.title}>{t('reservation:availabilityTitle')}</h2>\n <div className={styles.navigation}>\n <button className={styles.navButton} onClick={() => navigateWeek(-1)} type=\"button\">\n &larr;\n </button>\n <button className={styles.navButton} onClick={goToThisWeek} type=\"button\">\n {t('reservation:availabilityThisWeek')}\n </button>\n <button className={styles.navButton} onClick={() => navigateWeek(1)} type=\"button\">\n &rarr;\n </button>\n <span className={styles.weekLabel}>{weekLabel}</span>\n </div>\n\n {resources.length === 0 ? (\n <div className={styles.noResources}>{t('reservation:availabilityNoResources')}</div>\n ) : (\n <div className={styles.grid} style={{ gridTemplateColumns: gridColumns }}>\n {/* Header row */}\n <div className={styles.headerCell}>{t('reservation:availabilityResource')}</div>\n {weekDays.map((day, i) => {\n const dayKey = getDayKeyInTimezone(day, reservationTimezone)\n return (\n <div className={styles.headerCell} key={i}>\n {DAY_NAMES[DAY_MAP[getDayOfWeekFromDayKey(dayKey)]]} {Number(dayKey.slice(8, 10))}\n </div>\n )\n })}\n\n {/* Resource rows */}\n {resources.map((resource) => {\n const quantity = resource.quantity ?? 1\n return (\n <Fragment key={resource.id}>\n <div className={styles.resourceName}>\n {resource.name}\n {quantity > 1 && (\n <span style={{ fontWeight: 400, marginLeft: 4, opacity: 0.6 }}>\n {' '}(&times;{quantity})\n </span>\n )}\n </div>\n {weekDays.map((day, di) => {\n const slots = getSlotsForResourceDay(resource.id, day)\n const bookings = getBookingsForResourceDay(resource.id, day)\n const bookedCount = bookings.length\n\n return (\n <div className={styles.cell} key={`cell-${resource.id}-${di}`}>\n {slots.map((slot, si) => (\n <div\n className={\n slot.type === 'exception'\n ? styles.slotException\n : styles.slotAvailable\n }\n key={`slot-${si}`}\n >\n {slot.label}\n </div>\n ))}\n {quantity > 1 ? (\n /* Multi-unit resource: show X/Y booked with graduated color */\n bookedCount > 0 && (\n <div\n className={capacityClass(bookedCount, quantity)}\n title={t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n >\n {t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n </div>\n )\n ) : (\n /* Single-unit resource: show individual booking times */\n bookings.map((b) => (\n <div className={styles.slotBooked} key={b.id}>\n {new Date(b.startTime).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone: reservationTimezone,\n })}{' '}\n {t('reservation:availabilityBooked')}\n </div>\n ))\n )}\n </div>\n )\n })}\n </Fragment>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],"names":["useConfig","useTranslation","Fragment","useCallback","useEffect","useMemo","useState","getDayKeyInTimezone","getDayOfWeekFromDayKey","useTenantFilter","styles","DAY_MAP","fri","mon","sat","sun","thu","tue","wed","capacityClass","booked","total","slotCapacityFull","slotCapacityMid","slotCapacityLow","AvailabilityOverview","config","t","_t","slugs","admin","custom","reservationSlugs","statusMachine","reservationStatusMachine","blockingStatuses","reservationTimezone","resourcesTenantParams","resources","schedulesTenantParams","schedules","reservationsTenantParams","reservations","DAY_NAMES","weekStart","setWeekStart","now","Date","d","getFullYear","getMonth","getDate","setDate","getDay","setResources","setSchedules","setReservations","loading","setLoading","weekDays","Array","from","length","_","i","weekEnd","setHours","stale","fetchData","apiBase","serverURL","routes","api","blockingIn","join","resourcesParams","URLSearchParams","limit","schedulesParams","reservationsParams","depth","toISOString","resourcesRes","schedulesRes","reservationsRes","Promise","all","fetch","rData","sData","resData","json","docs","navigateWeek","direction","prev","next","goToThisWeek","getResourceId","r","id","getSlotsForResourceDay","resourceId","day","resourceSchedules","filter","s","resource","dateStr","dayOfWeek","slots","schedule","exception","exceptions","find","e","start","date","end","endDate","push","type","label","reason","scheduleType","slot","recurringSlots","startTime","endTime","manualSlots","slotDate","getBookingsForResourceDay","dayKey","div","className","noResources","weekLabel","toLocaleDateString","month","timeZone","year","gridColumns","wrapper","h2","title","navigation","button","navButton","onClick","span","grid","style","gridTemplateColumns","headerCell","map","Number","slice","quantity","resourceName","name","fontWeight","marginLeft","opacity","di","bookings","bookedCount","cell","si","slotException","slotAvailable","b","slotBooked","toLocaleTimeString","hour","minute"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/AvailabilityOverview/index.tsx"],"sourcesContent":["'use client'\nimport type { AdminViewServerProps } from 'payload'\n\nimport { useConfig, useTranslation } from '@payloadcms/ui'\nimport { Fragment, useCallback, useEffect, useMemo, useState } from 'react'\n\nimport type { PluginT } from '../../translations/index.js'\n\nimport { getDayKeyInTimezone, getDayOfWeekFromDayKey } from '../../utilities/timezoneUtils.js'\nimport { useTenantFilter } from '../../utilities/useTenantFilter.js'\nimport styles from './AvailabilityOverview.module.css'\n\ntype Resource = {\n active?: boolean\n capacityMode?: 'per-guest' | 'per-reservation'\n id: string\n name: string\n quantity?: number\n}\n\ntype Schedule = {\n active?: boolean\n exceptions?: Array<{ date: string; endDate?: string; reason?: string }>\n id: string\n manualSlots?: Array<{ date: string; endTime: string; startTime: string }>\n recurringSlots?: Array<{ day: string; endTime: string; startTime: string }>\n resource: { id: string } | string\n scheduleType: 'manual' | 'recurring'\n}\n\ntype Reservation = {\n endTime?: string\n id: string\n resource: { id: string } | string\n startTime: string\n status: string\n}\n\nconst DAY_MAP: Record<string, number> = {\n fri: 5,\n mon: 1,\n sat: 6,\n sun: 0,\n thu: 4,\n tue: 2,\n wed: 3,\n}\n\n/** Return the CSS class for a capacity badge based on utilization ratio. */\nfunction capacityClass(booked: number, total: number): string {\n if (booked >= total) {return styles.slotCapacityFull}\n if (booked / total >= 0.5) {return styles.slotCapacityMid}\n return styles.slotCapacityLow\n}\n\nexport const AvailabilityOverview: React.FC<AdminViewServerProps> = () => {\n const { config } = useConfig()\n const { t: _t } = useTranslation()\n const t = _t as PluginT\n const slugs = config.admin?.custom?.reservationSlugs\n const statusMachine = config.admin?.custom?.reservationStatusMachine\n const blockingStatuses: string[] = statusMachine?.blockingStatuses ?? ['pending', 'confirmed']\n // In multiTenant mode the business timezone is the SELECTED tenant's zone,\n // resolved server-side from the tenant cookie; fall back to the static global\n // zone until it resolves and for plain installs.\n const staticReservationTimezone: string = config.admin?.custom?.reservationTimezone ?? 'UTC'\n\n const resourcesTenantParams = useTenantFilter(slugs?.resources ?? 'resources')\n const schedulesTenantParams = useTenantFilter(slugs?.schedules ?? 'schedules')\n const reservationsTenantParams = useTenantFilter(slugs?.reservations ?? 'reservations')\n\n const [effectiveTimezone, setEffectiveTimezone] = useState<null | string>(null)\n const reservationTimezone = effectiveTimezone ?? staticReservationTimezone\n\n const tenantKey = JSON.stringify(reservationsTenantParams)\n useEffect(() => {\n let cancelled = false\n const apiBase = `${config.serverURL ?? ''}${config.routes.api}`\n void (async () => {\n try {\n const res = await fetch(`${apiBase}/reserve/effective-timezone`, {\n credentials: 'same-origin',\n })\n if (!res.ok) {\n return\n }\n const json = await res.json()\n if (!cancelled && typeof json?.timeZone === 'string') {\n setEffectiveTimezone(json.timeZone)\n }\n } catch {\n // keep the static fallback\n }\n })()\n return () => {\n cancelled = true\n }\n \n }, [config.serverURL, config.routes.api, tenantKey])\n\n const DAY_NAMES = useMemo(\n () => [\n t('reservation:dayShortSun'),\n t('reservation:dayShortMon'),\n t('reservation:dayShortTue'),\n t('reservation:dayShortWed'),\n t('reservation:dayShortThu'),\n t('reservation:dayShortFri'),\n t('reservation:dayShortSat'),\n ],\n [t],\n )\n\n const [weekStart, setWeekStart] = useState(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n return d\n })\n\n const [resources, setResources] = useState<Resource[]>([])\n const [schedules, setSchedules] = useState<Schedule[]>([])\n const [reservations, setReservations] = useState<Reservation[]>([])\n const [loading, setLoading] = useState(true)\n\n // Cells are browser-local midnights by design; every key derived from them\n // (day keys, weekday matching) is computed in the business timezone.\n const weekDays = useMemo(() => {\n return Array.from({ length: 7 }, (_, i) => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + i)\n return d\n })\n }, [weekStart])\n\n const weekEnd = useMemo(() => {\n const d = new Date(weekStart)\n d.setDate(d.getDate() + 6)\n d.setHours(23, 59, 59, 999)\n return d\n }, [weekStart])\n\n useEffect(() => {\n if (!slugs) {return}\n\n // Ignore a slow earlier fetch if the week changed before it resolved (D5)\n let stale = false\n\n const fetchData = async () => {\n setLoading(true)\n const apiBase = `${config.serverURL ?? ''}${config.routes.api}`\n\n // Build a query that fetches only blocking-status reservations so the\n // component doesn't need to filter client-side. The `in` operator on\n // Payload's REST API accepts a comma-separated list.\n const blockingIn = blockingStatuses.join(',')\n\n try {\n const resourcesParams = new URLSearchParams({\n limit: '100',\n 'where[active][equals]': 'true',\n ...resourcesTenantParams,\n })\n const schedulesParams = new URLSearchParams({\n limit: '1000',\n 'where[active][equals]': 'true',\n ...schedulesTenantParams,\n })\n const reservationsParams = new URLSearchParams({\n depth: '0',\n limit: '2000',\n 'where[startTime][greater_than_equal]': weekStart.toISOString(),\n 'where[startTime][less_than_equal]': weekEnd.toISOString(),\n 'where[status][in]': blockingIn,\n ...reservationsTenantParams,\n })\n const [resourcesRes, schedulesRes, reservationsRes] = await Promise.all([\n fetch(`${apiBase}/${slugs.resources}?${resourcesParams}`),\n fetch(`${apiBase}/${slugs.schedules}?${schedulesParams}`),\n fetch(`${apiBase}/${slugs.reservations}?${reservationsParams}`),\n ])\n\n const [rData, sData, resData] = await Promise.all([\n resourcesRes.json(),\n schedulesRes.json(),\n reservationsRes.json(),\n ])\n\n if (stale) {return}\n setResources(rData.docs ?? [])\n setSchedules(sData.docs ?? [])\n setReservations(resData.docs ?? [])\n } catch {\n if (stale) {return}\n setResources([])\n setSchedules([])\n setReservations([])\n }\n if (!stale) {setLoading(false)}\n }\n\n void fetchData()\n return () => {\n stale = true\n }\n // blockingStatuses is derived from config which is stable; stringify to\n // avoid object-reference churn causing infinite loops.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [weekStart, weekEnd, config.routes.api, config.serverURL, slugs, blockingStatuses.join(','), resourcesTenantParams, schedulesTenantParams, reservationsTenantParams])\n\n const navigateWeek = useCallback((direction: -1 | 1) => {\n setWeekStart((prev) => {\n const next = new Date(prev)\n next.setDate(next.getDate() + 7 * direction)\n return next\n })\n }, [])\n\n const goToThisWeek = useCallback(() => {\n const now = new Date()\n const d = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n d.setDate(d.getDate() - d.getDay())\n setWeekStart(d)\n }, [])\n\n const getResourceId = (r: { id: string } | string) =>\n typeof r === 'object' ? r.id : r\n\n const getSlotsForResourceDay = (resourceId: string, day: Date) => {\n const resourceSchedules = schedules.filter(\n (s) => getResourceId(s.resource) === resourceId,\n )\n const dateStr = getDayKeyInTimezone(day, reservationTimezone)\n const dayOfWeek = getDayOfWeekFromDayKey(dateStr)\n\n const slots: Array<{ label: string; type: 'available' | 'exception' }> = []\n\n for (const schedule of resourceSchedules) {\n // Check for exceptions — match the full [date, endDate] range inclusively\n // (review D4), keyed in the business timezone, like the server does.\n const exception = schedule.exceptions?.find((e) => {\n const start = getDayKeyInTimezone(new Date(e.date), reservationTimezone)\n const end = e.endDate ? getDayKeyInTimezone(new Date(e.endDate), reservationTimezone) : start\n return dateStr >= start && dateStr <= end\n })\n\n if (exception) {\n slots.push({\n type: 'exception',\n label: exception.reason || t('reservation:availabilityUnavailable'),\n })\n continue\n }\n\n if (schedule.scheduleType === 'recurring') {\n for (const slot of schedule.recurringSlots ?? []) {\n if (slot.day === dayOfWeek) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n } else if (schedule.scheduleType === 'manual') {\n for (const slot of schedule.manualSlots ?? []) {\n const slotDate = getDayKeyInTimezone(new Date(slot.date), reservationTimezone)\n if (slotDate === dateStr) {\n slots.push({\n type: 'available',\n label: `${slot.startTime}-${slot.endTime}`,\n })\n }\n }\n }\n }\n\n return slots\n }\n\n /** Returns all blocking-status reservations for a resource on a given day. */\n const getBookingsForResourceDay = (resourceId: string, day: Date) => {\n const dayKey = getDayKeyInTimezone(day, reservationTimezone)\n return reservations.filter((r) => {\n return (\n getResourceId(r.resource) === resourceId &&\n getDayKeyInTimezone(new Date(r.startTime), reservationTimezone) === dayKey\n )\n })\n }\n\n if (!slugs) {\n return <div className={styles.noResources}>{t('reservation:availabilityNotConfigured')}</div>\n }\n\n if (loading) {\n return <div className={styles.loading}>{t('reservation:availabilityLoading')}</div>\n }\n\n const weekLabel = `${weekDays[0].toLocaleDateString([], { day: 'numeric', month: 'short', timeZone: reservationTimezone })} - ${weekDays[6].toLocaleDateString([], { day: 'numeric', month: 'short', timeZone: reservationTimezone, year: 'numeric' })}`\n\n const gridColumns = `150px repeat(7, 1fr)`\n\n return (\n <div className={styles.wrapper}>\n <h2 className={styles.title}>{t('reservation:availabilityTitle')}</h2>\n <div className={styles.navigation}>\n <button className={styles.navButton} onClick={() => navigateWeek(-1)} type=\"button\">\n &larr;\n </button>\n <button className={styles.navButton} onClick={goToThisWeek} type=\"button\">\n {t('reservation:availabilityThisWeek')}\n </button>\n <button className={styles.navButton} onClick={() => navigateWeek(1)} type=\"button\">\n &rarr;\n </button>\n <span className={styles.weekLabel}>{weekLabel}</span>\n </div>\n\n {resources.length === 0 ? (\n <div className={styles.noResources}>{t('reservation:availabilityNoResources')}</div>\n ) : (\n <div className={styles.grid} style={{ gridTemplateColumns: gridColumns }}>\n {/* Header row */}\n <div className={styles.headerCell}>{t('reservation:availabilityResource')}</div>\n {weekDays.map((day, i) => {\n const dayKey = getDayKeyInTimezone(day, reservationTimezone)\n return (\n <div className={styles.headerCell} key={i}>\n {DAY_NAMES[DAY_MAP[getDayOfWeekFromDayKey(dayKey)]]} {Number(dayKey.slice(8, 10))}\n </div>\n )\n })}\n\n {/* Resource rows */}\n {resources.map((resource) => {\n const quantity = resource.quantity ?? 1\n return (\n <Fragment key={resource.id}>\n <div className={styles.resourceName}>\n {resource.name}\n {quantity > 1 && (\n <span style={{ fontWeight: 400, marginLeft: 4, opacity: 0.6 }}>\n {' '}(&times;{quantity})\n </span>\n )}\n </div>\n {weekDays.map((day, di) => {\n const slots = getSlotsForResourceDay(resource.id, day)\n const bookings = getBookingsForResourceDay(resource.id, day)\n const bookedCount = bookings.length\n\n return (\n <div className={styles.cell} key={`cell-${resource.id}-${di}`}>\n {slots.map((slot, si) => (\n <div\n className={\n slot.type === 'exception'\n ? styles.slotException\n : styles.slotAvailable\n }\n key={`slot-${si}`}\n >\n {slot.label}\n </div>\n ))}\n {quantity > 1 ? (\n /* Multi-unit resource: show X/Y booked with graduated color */\n bookedCount > 0 && (\n <div\n className={capacityClass(bookedCount, quantity)}\n title={t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n >\n {t('reservation:availabilityXofYBooked', {\n booked: bookedCount,\n total: quantity,\n })}\n </div>\n )\n ) : (\n /* Single-unit resource: show individual booking times */\n bookings.map((b) => (\n <div className={styles.slotBooked} key={b.id}>\n {new Date(b.startTime).toLocaleTimeString([], {\n hour: '2-digit',\n minute: '2-digit',\n timeZone: reservationTimezone,\n })}{' '}\n {t('reservation:availabilityBooked')}\n </div>\n ))\n )}\n </div>\n )\n })}\n </Fragment>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n"],"names":["useConfig","useTranslation","Fragment","useCallback","useEffect","useMemo","useState","getDayKeyInTimezone","getDayOfWeekFromDayKey","useTenantFilter","styles","DAY_MAP","fri","mon","sat","sun","thu","tue","wed","capacityClass","booked","total","slotCapacityFull","slotCapacityMid","slotCapacityLow","AvailabilityOverview","config","t","_t","slugs","admin","custom","reservationSlugs","statusMachine","reservationStatusMachine","blockingStatuses","staticReservationTimezone","reservationTimezone","resourcesTenantParams","resources","schedulesTenantParams","schedules","reservationsTenantParams","reservations","effectiveTimezone","setEffectiveTimezone","tenantKey","JSON","stringify","cancelled","apiBase","serverURL","routes","api","res","fetch","credentials","ok","json","timeZone","DAY_NAMES","weekStart","setWeekStart","now","Date","d","getFullYear","getMonth","getDate","setDate","getDay","setResources","setSchedules","setReservations","loading","setLoading","weekDays","Array","from","length","_","i","weekEnd","setHours","stale","fetchData","blockingIn","join","resourcesParams","URLSearchParams","limit","schedulesParams","reservationsParams","depth","toISOString","resourcesRes","schedulesRes","reservationsRes","Promise","all","rData","sData","resData","docs","navigateWeek","direction","prev","next","goToThisWeek","getResourceId","r","id","getSlotsForResourceDay","resourceId","day","resourceSchedules","filter","s","resource","dateStr","dayOfWeek","slots","schedule","exception","exceptions","find","e","start","date","end","endDate","push","type","label","reason","scheduleType","slot","recurringSlots","startTime","endTime","manualSlots","slotDate","getBookingsForResourceDay","dayKey","div","className","noResources","weekLabel","toLocaleDateString","month","year","gridColumns","wrapper","h2","title","navigation","button","navButton","onClick","span","grid","style","gridTemplateColumns","headerCell","map","Number","slice","quantity","resourceName","name","fontWeight","marginLeft","opacity","di","bookings","bookedCount","cell","si","slotException","slotAvailable","b","slotBooked","toLocaleTimeString","hour","minute"],"mappings":"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"}
@@ -76,7 +76,39 @@ export const CalendarView = ()=>{
76
76
  const resourceSlug = slugs?.resources ?? 'resources';
77
77
  const reservationTenantParams = useTenantFilter(reservationSlug);
78
78
  const resourceTenantParams = useTenantFilter(resourceSlug);
79
- const reservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
79
+ // Day-boundary rendering uses the business timezone. In multiTenant mode that's
80
+ // the SELECTED tenant's zone, resolved server-side from the tenant cookie (the
81
+ // client can't map tenant→zone itself). Until that resolves — and for plain
82
+ // installs — fall back to the static global zone baked into admin config.
83
+ const staticReservationTimezone = config.admin?.custom?.reservationTimezone ?? 'UTC';
84
+ const [effectiveTimezone, setEffectiveTimezone] = useState(null);
85
+ const reservationTimezone = effectiveTimezone ?? staticReservationTimezone;
86
+ const tenantKey = JSON.stringify(reservationTenantParams);
87
+ useEffect(()=>{
88
+ let cancelled = false;
89
+ void (async ()=>{
90
+ try {
91
+ const res = await fetch(`${apiBase}/reserve/effective-timezone`, {
92
+ credentials: 'same-origin'
93
+ });
94
+ if (!res.ok) {
95
+ return;
96
+ }
97
+ const json = await res.json();
98
+ if (!cancelled && typeof json?.timeZone === 'string') {
99
+ setEffectiveTimezone(json.timeZone);
100
+ }
101
+ } catch {
102
+ // keep the static fallback
103
+ }
104
+ })();
105
+ return ()=>{
106
+ cancelled = true;
107
+ };
108
+ }, [
109
+ apiBase,
110
+ tenantKey
111
+ ]);
80
112
  const statusMachine = config.admin?.custom?.reservationStatusMachine;
81
113
  // The initial/pending status (what "pending" view shows)
82
114
  const defaultStatus = statusMachine?.defaultStatus ?? 'pending';