payload-reserve 1.5.0 → 2.0.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.
Files changed (95) hide show
  1. package/README.md +40 -3
  2. package/dist/collections/Reservations.js +19 -7
  3. package/dist/collections/Reservations.js.map +1 -1
  4. package/dist/collections/Resources.js +11 -8
  5. package/dist/collections/Resources.js.map +1 -1
  6. package/dist/collections/Schedules.js +12 -6
  7. package/dist/collections/Schedules.js.map +1 -1
  8. package/dist/collections/Services.js +19 -10
  9. package/dist/collections/Services.js.map +1 -1
  10. package/dist/components/AvailabilityOverview/index.js +70 -26
  11. package/dist/components/AvailabilityOverview/index.js.map +1 -1
  12. package/dist/components/CalendarView/CalendarView.module.css +9 -0
  13. package/dist/components/CalendarView/LaneTimelineView.d.ts +4 -1
  14. package/dist/components/CalendarView/LaneTimelineView.js +17 -12
  15. package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
  16. package/dist/components/CalendarView/index.js +154 -53
  17. package/dist/components/CalendarView/index.js.map +1 -1
  18. package/dist/components/CustomerField/index.js +8 -3
  19. package/dist/components/CustomerField/index.js.map +1 -1
  20. package/dist/components/DashboardWidget/DashboardWidgetServer.js +97 -21
  21. package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
  22. package/dist/defaults.js +46 -8
  23. package/dist/defaults.js.map +1 -1
  24. package/dist/endpoints/cancelBooking.js +1 -1
  25. package/dist/endpoints/cancelBooking.js.map +1 -1
  26. package/dist/endpoints/checkAvailability.js +56 -7
  27. package/dist/endpoints/checkAvailability.js.map +1 -1
  28. package/dist/endpoints/createBooking.js +19 -10
  29. package/dist/endpoints/createBooking.js.map +1 -1
  30. package/dist/endpoints/customerSearch.js +5 -2
  31. package/dist/endpoints/customerSearch.js.map +1 -1
  32. package/dist/endpoints/getSlots.js +56 -7
  33. package/dist/endpoints/getSlots.js.map +1 -1
  34. package/dist/endpoints/resourceAvailability.d.ts +2 -1
  35. package/dist/endpoints/resourceAvailability.js +85 -25
  36. package/dist/endpoints/resourceAvailability.js.map +1 -1
  37. package/dist/hooks/reservations/calculateEndTime.js +48 -20
  38. package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
  39. package/dist/hooks/reservations/enforceCustomerOwnership.d.ts +11 -0
  40. package/dist/hooks/reservations/enforceCustomerOwnership.js +30 -0
  41. package/dist/hooks/reservations/enforceCustomerOwnership.js.map +1 -0
  42. package/dist/hooks/reservations/onStatusChange.js +10 -4
  43. package/dist/hooks/reservations/onStatusChange.js.map +1 -1
  44. package/dist/hooks/reservations/validateCancellation.js +3 -2
  45. package/dist/hooks/reservations/validateCancellation.js.map +1 -1
  46. package/dist/hooks/reservations/validateConflicts.js +23 -4
  47. package/dist/hooks/reservations/validateConflicts.js.map +1 -1
  48. package/dist/hooks/reservations/validateGuestBooking.js +3 -4
  49. package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
  50. package/dist/hooks/reservations/validateStatusTransition.js +2 -2
  51. package/dist/hooks/reservations/validateStatusTransition.js.map +1 -1
  52. package/dist/hooks/users/provisionStaffResource.js +5 -8
  53. package/dist/hooks/users/provisionStaffResource.js.map +1 -1
  54. package/dist/plugin.js +82 -13
  55. package/dist/plugin.js.map +1 -1
  56. package/dist/services/AvailabilityService.d.ts +54 -2
  57. package/dist/services/AvailabilityService.js +180 -46
  58. package/dist/services/AvailabilityService.js.map +1 -1
  59. package/dist/translations/ar.json +1 -0
  60. package/dist/translations/de.json +1 -0
  61. package/dist/translations/en.json +1 -0
  62. package/dist/translations/es.json +1 -0
  63. package/dist/translations/fa.json +1 -0
  64. package/dist/translations/fr.json +1 -0
  65. package/dist/translations/hi.json +1 -0
  66. package/dist/translations/id.json +1 -0
  67. package/dist/translations/pl.json +1 -0
  68. package/dist/translations/ru.json +1 -0
  69. package/dist/translations/tr.json +1 -0
  70. package/dist/translations/zh.json +1 -0
  71. package/dist/types.d.ts +50 -1
  72. package/dist/types.js +2 -0
  73. package/dist/types.js.map +1 -1
  74. package/dist/utilities/collectionOverrides.d.ts +14 -0
  75. package/dist/utilities/collectionOverrides.js +47 -0
  76. package/dist/utilities/collectionOverrides.js.map +1 -0
  77. package/dist/utilities/ownerAccess.d.ts +6 -0
  78. package/dist/utilities/ownerAccess.js +25 -12
  79. package/dist/utilities/ownerAccess.js.map +1 -1
  80. package/dist/utilities/reservationChanges.d.ts +17 -0
  81. package/dist/utilities/reservationChanges.js +88 -0
  82. package/dist/utilities/reservationChanges.js.map +1 -0
  83. package/dist/utilities/scheduleUtils.d.ts +14 -8
  84. package/dist/utilities/scheduleUtils.js +26 -19
  85. package/dist/utilities/scheduleUtils.js.map +1 -1
  86. package/dist/utilities/tenantFilter.d.ts +25 -0
  87. package/dist/utilities/tenantFilter.js +56 -0
  88. package/dist/utilities/tenantFilter.js.map +1 -0
  89. package/dist/utilities/timezoneUtils.d.ts +39 -0
  90. package/dist/utilities/timezoneUtils.js +134 -0
  91. package/dist/utilities/timezoneUtils.js.map +1 -0
  92. package/dist/utilities/useTenantFilter.d.ts +6 -0
  93. package/dist/utilities/useTenantFilter.js +28 -0
  94. package/dist/utilities/useTenantFilter.js.map +1 -0
  95. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.disabled) {\n return config\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (targetCollection) {\n // Collect existing field names for deduplication check\n const existingFieldNames = new Set(\n targetCollection.fields\n .map((field) => ('name' in field ? field.name : undefined))\n .filter(Boolean),\n )\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n required: true,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n\n // Push only the 4 domain collections (no standalone Customers)\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n )\n } else {\n // Default behaviour: push all 5 collections including standalone Customers\n config.collections.push(\n createServicesCollection(resolved),\n createResourcesCollection(resolved),\n createSchedulesCollection(resolved),\n createReservationsCollection(resolved),\n createCustomersCollection(resolved),\n )\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createGetSlotsEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","disabled","userCollection","targetCollection","find","col","slug","existingFieldNames","Set","fields","map","field","name","undefined","filter","Boolean","fieldsToAdd","type","maxLength","required","collection","slugs","reservations","on","fieldName","has","push","customers","endpoints","staffProvisioning","staffUserSlug","staffCollection","Error","hooks","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","path","i18n"],"mappings":"AAEA,SAASA,eAAe,QAAQ,iBAAgB;AAIhD,SAASC,yBAAyB,QAAQ,6BAA4B;AACtE,SAASC,4BAA4B,QAAQ,gCAA+B;AAC5E,SAASC,yBAAyB,QAAQ,6BAA4B;AACtE,SAASC,yBAAyB,QAAQ,6BAA4B;AACtE,SAASC,wBAAwB,QAAQ,4BAA2B;AACpE,SAASC,aAAa,QAAQ,gBAAe;AAC7C,SAASC,2BAA2B,QAAQ,+BAA8B;AAC1E,SAASC,+BAA+B,QAAQ,mCAAkC;AAClF,SAASC,qBAAqB,QAAQ,+BAA8B;AACpE,SAASC,4BAA4B,QAAQ,gCAA+B;AAC5E,SAASC,sBAAsB,QAAQ,0BAAyB;AAChE,SAASC,kCAAkC,QAAQ,sCAAqC;AACxF,SAASC,sBAAsB,QAAQ,0CAAyC;AAChF,SAAuBC,YAAY,QAAQ,0BAAyB;AAEpE,OAAO,MAAMC,iBACX,CAACC,gBAAyC,CAAC,CAAC,GAC5C,CAACC;QACC,MAAMC,WAAWZ,cAAcU;QAE/B,8CAA8C;QAC9C,IAAIC,OAAOE,YAAY,EAAE;YACvBD,SAASE,SAAS,GAAG;QACvB;QAEA,IAAI,CAACH,OAAOI,WAAW,EAAE;YACvBJ,OAAOI,WAAW,GAAG,EAAE;QACzB;QAEA,IAAIH,SAASI,QAAQ,EAAE;YACrB,OAAOL;QACT;QAEA,IAAIC,SAASK,cAAc,EAAE;YAC3B,2DAA2D;YAC3D,MAAMC,mBAAmBP,OAAOI,WAAW,CAACI,IAAI,CAC9C,CAACC,MAAQA,IAAIC,IAAI,KAAKT,SAASK,cAAc;YAG/C,IAAIC,kBAAkB;gBACpB,uDAAuD;gBACvD,MAAMI,qBAAqB,IAAIC,IAC7BL,iBAAiBM,MAAM,CACpBC,GAAG,CAAC,CAACC,QAAW,UAAUA,QAAQA,MAAMC,IAAI,GAAGC,WAC/CC,MAAM,CAACC;gBAGZ,mEAAmE;gBACnE,qEAAqE;gBACrE,wEAAwE;gBACxE,MAAMC,cAAuB;oBAC3B;wBACEJ,MAAM;wBACNK,MAAM;wBACNC,WAAW;wBACXC,UAAU;oBACZ;oBACA;wBACEP,MAAM;wBACNK,MAAM;wBACNC,WAAW;oBACb;oBACA;wBACEN,MAAM;wBACNK,MAAM;oBACR;oBACA;wBACEL,MAAM;wBACNK,MAAM;wBACNG,YAAYvB,SAASwB,KAAK,CAACC,YAAY;wBACvCC,IAAI;oBACN;iBACD;gBAED,KAAK,MAAMZ,SAASK,YAAa;oBAC/B,MAAMQ,YAAY,UAAUb,QAAQA,MAAMC,IAAI,GAAGC;oBACjD,IAAIW,aAAa,CAACjB,mBAAmBkB,GAAG,CAACD,YAAY;wBACnDrB,iBAAiBM,MAAM,CAACiB,IAAI,CAACf;oBAC/B;gBACF;YACF;YAEA,wEAAwE;YACxE,6DAA6D;YAC7Dd,SAASwB,KAAK,CAACM,SAAS,GAAG9B,SAASK,cAAc;YAElD,+DAA+D;YAC/DN,OAAOI,WAAW,CAAC0B,IAAI,CACrB1C,yBAAyBa,WACzBf,0BAA0Be,WAC1Bd,0BAA0Bc,WAC1BhB,6BAA6BgB;QAEjC,OAAO;YACL,2EAA2E;YAC3ED,OAAOI,WAAW,CAAC0B,IAAI,CACrB1C,yBAAyBa,WACzBf,0BAA0Be,WAC1Bd,0BAA0Bc,WAC1BhB,6BAA6BgB,WAC7BjB,0BAA0BiB;QAE9B;QAEA,4BAA4B;QAC5B,IAAI,CAACD,OAAOgC,SAAS,EAAE;YAAChC,OAAOgC,SAAS,GAAG,EAAE;QAAA;QAC7ChC,OAAOgC,SAAS,CAACF,IAAI,CACnBxC,4BAA4BW,WAC5BV,gCAAgCU,WAChCT,sBAAsBS,WACtBR,6BAA6BQ,WAC7BP,uBAAuBO,WACvBN,mCAAmCM;QAGrC,8DAA8D;QAC9D,IAAIA,SAASgC,iBAAiB,EAAE;YAC9B,MAAMC,gBAAgBjC,SAASgC,iBAAiB,CAAC3B,cAAc;YAC/D,MAAM6B,kBAAkBnC,OAAOI,WAAW,CAACI,IAAI,CAAC,CAACC,MAAQA,IAAIC,IAAI,KAAKwB;YACtE,IAAI,CAACC,iBAAiB;gBACpB,MAAM,IAAIC,MACR,CAAC,kCAAkC,EAAEF,cAAc,qCAAqC,CAAC;YAE7F;YACAC,gBAAgBE,KAAK,GAAG;gBACtB,GAAGF,gBAAgBE,KAAK;gBACxBC,aAAa;uBACPH,gBAAgBE,KAAK,EAAEC,eAAe,EAAE;oBAC5C1C,uBAAuBK;iBACxB;YACH;QACF;QAEA,6BAA6B;QAC7B,IAAI,CAACD,OAAOuC,KAAK,EAAE;YAACvC,OAAOuC,KAAK,GAAG,CAAC;QAAC;QACrC,IAAI,CAACvC,OAAOuC,KAAK,CAACC,UAAU,EAAE;YAACxC,OAAOuC,KAAK,CAACC,UAAU,GAAG,CAAC;QAAC;QAE3D,sEAAsE;QACtE,IAAI,CAACxC,OAAOuC,KAAK,CAACE,MAAM,EAAE;YAACzC,OAAOuC,KAAK,CAACE,MAAM,GAAG,CAAC;QAAC;QACnDzC,OAAOuC,KAAK,CAACE,MAAM,CAACC,gBAAgB,GAAG;YACrC,GAAGzC,SAASwB,KAAK;QACnB;QACAzB,OAAOuC,KAAK,CAACE,MAAM,CAACE,wBAAwB,GAAG1C,SAAS2C,aAAa;QAErE,uBAAuB;QACvB,IAAI,CAAC5C,OAAOuC,KAAK,CAACM,SAAS,EAAE;YAC3B7C,OAAOuC,KAAK,CAACM,SAAS,GAAG;gBAAEC,SAAS,EAAE;YAAC;QACzC;QACA,IAAI,CAAC9C,OAAOuC,KAAK,CAACM,SAAS,CAACC,OAAO,EAAE;YACnC9C,OAAOuC,KAAK,CAACM,SAAS,CAACC,OAAO,GAAG,EAAE;QACrC;QACA9C,OAAOuC,KAAK,CAACM,SAAS,CAACC,OAAO,CAAChB,IAAI,CAAC;YAClCpB,MAAM;YACNqC,WAAW;YACXC,OAAO,CAAC,EAAEC,CAAC,EAAE,GAAK,AAACA,EAAc;YACjCC,UAAU;YACVC,UAAU;QACZ;QAEA,iDAAiD;QACjD,IAAI,CAACnD,OAAOuC,KAAK,CAACC,UAAU,CAACY,KAAK,EAAE;YAClCpD,OAAOuC,KAAK,CAACC,UAAU,CAACY,KAAK,GAAG,CAAC;QACnC;;QACEpD,OAAOuC,KAAK,CAACC,UAAU,CAACY,KAAK,AAA4B,CAAC,2BAA2B,GAAG;YACxFL,WAAW;YACXM,MAAM;QACR;QAEA,gEAAgE;QAChErD,OAAOsD,IAAI,GAAG;YACZ,GAAItD,OAAOsD,IAAI,IAAI,CAAC,CAAC;YACrBzD,cAAcd,gBACZc,cACA,AAACG,OAAOsD,IAAI,EAAEzD,gBAA4D,CAAC;QAE/E;QAEA,OAAOG;IACT,EAAC"}
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\nimport { applyCollectionOverride } from './utilities/collectionOverrides.js'\n\n/**\n * All named field paths reachable from a field list, descending through\n * presentational containers (tabs, rows, collapsibles, unnamed groups) that\n * don't create their own data nesting — so dedup catches a field declared\n * inside one of them. Named groups/arrays DO nest data, so we don't recurse\n * into them (a `name` inside a named group is a different path).\n */\nfunction collectFieldNames(fields: Field[]): Set<string> {\n const names = new Set<string>()\n const walk = (list: Field[]): void => {\n for (const field of list) {\n if ('name' in field && field.name) {\n names.add(field.name)\n } else if ('tabs' in field && Array.isArray(field.tabs)) {\n for (const tab of field.tabs) {\n if ('name' in tab && tab.name) {\n names.add(tab.name)\n } else if (Array.isArray(tab.fields)) {\n walk(tab.fields)\n }\n }\n } else if ('fields' in field && Array.isArray(field.fields)) {\n // row / collapsible / unnamed group\n walk(field.fields)\n }\n }\n }\n walk(fields)\n return names\n}\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (!targetCollection) {\n // Fail loudly rather than silently skipping field injection and pointing\n // the customers slug at a collection that doesn't exist (review C2).\n throw new Error(\n `payload-reserve: userCollection \"${resolved.userCollection}\" was not found in config.collections. ` +\n `Define it before payloadReserve() runs, or correct the slug.`,\n )\n }\n\n {\n // Collect existing field names — descend into presentational containers\n // (tabs/rows/collapsibles/groups) so a field nested there isn't\n // re-injected at the top level (review C4).\n const existingFieldNames = collectFieldNames(targetCollection.fields)\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n // It is NOT required — an existing users collection may have rows\n // without a name, and forcing required would fail their next update (C4).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n }\n\n // The slugs this plugin is about to register (Customers only in standalone mode)\n const slugsToRegister = [\n resolved.slugs.services,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.reservations,\n ...(resolved.userCollection ? [] : [resolved.slugs.customers]),\n ]\n\n // C11: fail with a clear, actionable error on slug collision instead of\n // Payload's generic DuplicateCollection throw.\n for (const slug of slugsToRegister) {\n if (config.collections.some((col) => col.slug === slug)) {\n throw new Error(\n `payload-reserve: a collection with slug \"${slug}\" already exists. ` +\n `Override the plugin's slug via the \\`slugs\\` option.`,\n )\n }\n }\n\n // Image upload fields are added only when the media collection actually\n // exists, so installs without one don't hit an opaque init error (C8).\n resolved.hasMediaCollection = config.collections.some(\n (col) => col.slug === resolved.slugs.media,\n )\n\n const ov = resolved.collectionOverrides\n config.collections.push(\n applyCollectionOverride(createServicesCollection(resolved), ov.services),\n applyCollectionOverride(createResourcesCollection(resolved), ov.resources),\n applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules),\n applyCollectionOverride(createReservationsCollection(resolved), ov.reservations),\n // The customers override applies only in standalone mode; in userCollection\n // mode the host owns that collection and can edit it directly.\n ...(resolved.userCollection\n ? []\n : [applyCollectionOverride(createCustomersCollection(resolved), ov.customers)]),\n )\n\n // C3: collections are registered (above) even when disabled so the DB schema\n // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.\n if (resolved.disabled) {\n for (const slug of slugsToRegister) {\n const col = config.collections.find((c) => c.slug === slug)\n if (col) {\n delete col.hooks\n }\n }\n return config\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n config.admin.custom.reservationTenant = resolved.multiTenant\n config.admin.custom.reservationTimezone = resolved.timezone\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createGetSlotsEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","applyCollectionOverride","collectFieldNames","fields","names","Set","walk","list","field","name","add","Array","isArray","tabs","tab","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","userCollection","targetCollection","find","col","slug","Error","existingFieldNames","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","fieldName","undefined","has","push","customers","slugsToRegister","services","resources","schedules","some","hasMediaCollection","media","ov","collectionOverrides","disabled","c","hooks","endpoints","staffProvisioning","staffUserSlug","staffCollection","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","reservationTenant","multiTenant","reservationTimezone","timezone","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","path","i18n"],"mappings":"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"}
@@ -1,10 +1,18 @@
1
1
  import type { Payload, PayloadRequest, Where } from 'payload';
2
- import type { DurationType, StatusMachineConfig } from '../types.js';
2
+ import type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js';
3
+ import type { ResolvedItem } from '../utilities/resolveReservationItems.js';
4
+ /** A window during which a resource is occupied, expanded by buffer times. */
5
+ export type Occupancy = {
6
+ blockedEnd: Date;
7
+ blockedStart: Date;
8
+ units: number;
9
+ };
3
10
  export declare function computeEndTime(params: {
4
11
  durationType: DurationType;
5
12
  endTime?: Date;
6
13
  serviceDuration: number;
7
14
  startTime: Date;
15
+ timeZone?: string;
8
16
  }): {
9
17
  durationMinutes: number;
10
18
  endTime: Date;
@@ -16,6 +24,46 @@ export declare function buildOverlapQuery(params: {
16
24
  excludeReservationId?: number | string;
17
25
  resourceId: number | string;
18
26
  }): Where;
27
+ /**
28
+ * Coarse superset query: blocking reservations whose top-level (span) window
29
+ * comes within COARSE_MARGIN_MS of the candidate window and reference the
30
+ * resource at top level or in items[]. The precise per-item overlap is computed
31
+ * in memory afterwards — the top-level span is a superset of every item's
32
+ * window, so this never misses a real conflict (margin covers neighbor buffers).
33
+ */
34
+ export declare function buildCoarseOverlapQuery(params: {
35
+ blockingStatuses: string[];
36
+ candidateEnd: Date;
37
+ candidateStart: Date;
38
+ excludeReservationId?: number | string;
39
+ resourceId: number | string;
40
+ }): Where;
41
+ /**
42
+ * The occupancy windows a set of resolved items imposes on `resourceId`. Each
43
+ * matching item's [startTime, endTime) is expanded by that item's own service
44
+ * buffers (so neighbor buffers are enforced — review A3), and only the items
45
+ * that actually reference `resourceId` count (so a multi-resource booking blocks
46
+ * each resource only for its own item's window — review A4).
47
+ */
48
+ export declare function itemsToOccupancies(params: {
49
+ bufferFor: (serviceId: number | string | undefined) => Promise<{
50
+ after: number;
51
+ before: number;
52
+ }>;
53
+ capacityMode: CapacityMode;
54
+ items: ResolvedItem[];
55
+ resourceId: number | string;
56
+ }): Promise<Occupancy[]>;
57
+ /** Occupancy a single fetched reservation imposes on `resourceId`. */
58
+ export declare function reservationOccupancies(params: {
59
+ bufferFor: (serviceId: number | string | undefined) => Promise<{
60
+ after: number;
61
+ before: number;
62
+ }>;
63
+ capacityMode: CapacityMode;
64
+ reservation: Record<string, unknown>;
65
+ resourceId: number | string;
66
+ }): Promise<Occupancy[]>;
19
67
  export declare function isBlockingStatus(status: string, statusMachine: StatusMachineConfig): boolean;
20
68
  export declare function validateTransition(fromStatus: string, toStatus: string, statusMachine: StatusMachineConfig): {
21
69
  reason?: string;
@@ -33,6 +81,9 @@ export declare function checkAvailability(params: {
33
81
  reservationSlug: string;
34
82
  resourceId: number | string;
35
83
  resourceSlug: string;
84
+ servicesSlug: string;
85
+ /** Other items from the same booking — counted as occupancy (review A5). */
86
+ siblingItems?: ResolvedItem[];
36
87
  startTime: Date;
37
88
  }): Promise<{
38
89
  available: boolean;
@@ -42,7 +93,7 @@ export declare function checkAvailability(params: {
42
93
  }>;
43
94
  export declare function getAvailableSlots(params: {
44
95
  blockingStatuses: string[];
45
- date: Date;
96
+ date: Date | string;
46
97
  guestCount?: number;
47
98
  payload: Payload;
48
99
  req: PayloadRequest;
@@ -53,6 +104,7 @@ export declare function getAvailableSlots(params: {
53
104
  scheduleSlug: string;
54
105
  serviceId: number | string;
55
106
  serviceSlug: string;
107
+ timeZone?: string;
56
108
  }): Promise<Array<{
57
109
  end: Date;
58
110
  start: Date;
@@ -1,11 +1,14 @@
1
- import { resolveScheduleForDate } from '../utilities/scheduleUtils.js';
2
- import { addMinutes, computeBlockedWindow, intersectIntervals } from '../utilities/slotUtils.js';
1
+ import { resolveReservationItems } from '../utilities/resolveReservationItems.js';
2
+ import { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js';
3
+ import { addMinutes, computeBlockedWindow, doRangesOverlap, intersectIntervals } from '../utilities/slotUtils.js';
4
+ import { endOfDayInTimezone } from '../utilities/timezoneUtils.js';
5
+ /** Coarse pre-filter widen: covers any realistic neighbor buffer (buffers are
6
+ * minutes). The precise per-item overlap check runs in memory afterwards. */ const COARSE_MARGIN_MS = 24 * 60 * 60 * 1000;
3
7
  // --- Pure functions (no DB) ---
4
8
  export function computeEndTime(params) {
5
9
  const { durationType, serviceDuration, startTime } = params;
6
10
  if (durationType === 'full-day') {
7
- const end = new Date(startTime);
8
- end.setHours(23, 59, 59, 999);
11
+ const end = endOfDayInTimezone(startTime, params.timeZone ?? 'UTC');
9
12
  const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000);
10
13
  return {
11
14
  durationMinutes,
@@ -70,6 +73,90 @@ export function buildOverlapQuery(params) {
70
73
  and: conditions
71
74
  };
72
75
  }
76
+ /**
77
+ * Coarse superset query: blocking reservations whose top-level (span) window
78
+ * comes within COARSE_MARGIN_MS of the candidate window and reference the
79
+ * resource at top level or in items[]. The precise per-item overlap is computed
80
+ * in memory afterwards — the top-level span is a superset of every item's
81
+ * window, so this never misses a real conflict (margin covers neighbor buffers).
82
+ */ export function buildCoarseOverlapQuery(params) {
83
+ const { blockingStatuses, candidateEnd, candidateStart, excludeReservationId, resourceId } = params;
84
+ const windowStart = new Date(candidateStart.getTime() - COARSE_MARGIN_MS);
85
+ const windowEnd = new Date(candidateEnd.getTime() + COARSE_MARGIN_MS);
86
+ const conditions = [
87
+ {
88
+ status: {
89
+ in: blockingStatuses
90
+ }
91
+ },
92
+ {
93
+ startTime: {
94
+ less_than: windowEnd.toISOString()
95
+ }
96
+ },
97
+ {
98
+ endTime: {
99
+ greater_than: windowStart.toISOString()
100
+ }
101
+ },
102
+ {
103
+ or: [
104
+ {
105
+ resource: {
106
+ equals: resourceId
107
+ }
108
+ },
109
+ {
110
+ 'items.resource': {
111
+ equals: resourceId
112
+ }
113
+ }
114
+ ]
115
+ }
116
+ ];
117
+ if (excludeReservationId !== undefined) {
118
+ conditions.push({
119
+ id: {
120
+ not_equals: excludeReservationId
121
+ }
122
+ });
123
+ }
124
+ return {
125
+ and: conditions
126
+ };
127
+ }
128
+ /**
129
+ * The occupancy windows a set of resolved items imposes on `resourceId`. Each
130
+ * matching item's [startTime, endTime) is expanded by that item's own service
131
+ * buffers (so neighbor buffers are enforced — review A3), and only the items
132
+ * that actually reference `resourceId` count (so a multi-resource booking blocks
133
+ * each resource only for its own item's window — review A4).
134
+ */ export async function itemsToOccupancies(params) {
135
+ const { bufferFor, capacityMode, items, resourceId } = params;
136
+ const occupancies = [];
137
+ for (const item of items){
138
+ if (String(item.resource) !== String(resourceId) || !item.endTime) {
139
+ continue;
140
+ }
141
+ const { after, before } = await bufferFor(item.service);
142
+ const { effectiveEnd, effectiveStart } = computeBlockedWindow(new Date(item.startTime), new Date(item.endTime), before, after);
143
+ occupancies.push({
144
+ blockedEnd: effectiveEnd,
145
+ blockedStart: effectiveStart,
146
+ units: capacityMode === 'per-guest' ? item.guestCount : 1
147
+ });
148
+ }
149
+ return occupancies;
150
+ }
151
+ /** Occupancy a single fetched reservation imposes on `resourceId`. */ export async function reservationOccupancies(params) {
152
+ const { bufferFor, capacityMode, reservation, resourceId } = params;
153
+ return itemsToOccupancies({
154
+ bufferFor,
155
+ capacityMode,
156
+ items: resolveReservationItems(reservation),
157
+ resourceId
158
+ });
159
+ }
73
160
  export function isBlockingStatus(status, statusMachine) {
74
161
  return statusMachine.blockingStatuses.includes(status);
75
162
  }
@@ -93,7 +180,7 @@ export function validateTransition(fromStatus, toStatus, statusMachine) {
93
180
  }
94
181
  // --- DB functions (use Payload Local API only) ---
95
182
  export async function checkAvailability(params) {
96
- const { blockingStatuses, bufferAfter, bufferBefore, endTime, excludeReservationId, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, startTime } = params;
183
+ const { blockingStatuses, bufferAfter, bufferBefore, endTime, excludeReservationId, guestCount, payload, req, reservationSlug, resourceId, resourceSlug, servicesSlug, siblingItems, startTime } = params;
97
184
  // Fetch resource for quantity and capacity mode
98
185
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
186
  const resource = await payload.findByID({
@@ -104,54 +191,89 @@ export async function checkAvailability(params) {
104
191
  });
105
192
  const quantity = resource.quantity ?? 1;
106
193
  const capacityMode = resource.capacityMode ?? 'per-reservation';
107
- // Compute effective window with buffers
108
- const { effectiveEnd, effectiveStart } = computeBlockedWindow(startTime, endTime, bufferBefore, bufferAfter);
109
- // Build overlap query
110
- const where = buildOverlapQuery({
111
- blockingStatuses,
112
- effectiveEnd,
113
- effectiveStart,
114
- excludeReservationId,
115
- resourceId
116
- });
117
- if (capacityMode === 'per-guest') {
118
- // Must fetch docs to sum guestCount
119
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
- const { docs } = await payload.find({
121
- collection: reservationSlug,
122
- depth: 0,
123
- limit: 0,
124
- req,
125
- select: {
126
- guestCount: true
127
- },
128
- where
129
- });
130
- const currentGuests = docs.reduce((sum, doc)=>sum + (doc.guestCount ?? 1), 0);
131
- return {
132
- available: currentGuests + guestCount <= quantity,
133
- currentCount: currentGuests,
134
- reason: currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,
135
- totalCapacity: quantity
136
- };
137
- }
138
- // per-reservation mode: count is sufficient
139
- // TODO: batch queries — linear per-item cost acceptable for 2-5 items
194
+ // Candidate window expanded by its own buffers
195
+ const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(startTime, endTime, bufferBefore, bufferAfter);
196
+ // Coarse superset fetch — precise per-item overlap is computed in memory below
140
197
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
141
- const { totalDocs } = await payload.count({
198
+ const { docs } = await payload.find({
142
199
  collection: reservationSlug,
200
+ depth: 0,
201
+ limit: 0,
143
202
  req,
144
- where
203
+ where: buildCoarseOverlapQuery({
204
+ blockingStatuses,
205
+ candidateEnd,
206
+ candidateStart,
207
+ excludeReservationId,
208
+ resourceId
209
+ })
145
210
  });
211
+ // Per-call cache: fetch each distinct service's buffers at most once
212
+ const bufferCache = new Map();
213
+ const bufferFor = async (serviceId)=>{
214
+ const key = serviceId === undefined ? '' : String(serviceId);
215
+ const cached = bufferCache.get(key);
216
+ if (cached) {
217
+ return cached;
218
+ }
219
+ let result = {
220
+ after: 0,
221
+ before: 0
222
+ };
223
+ if (serviceId !== undefined) {
224
+ try {
225
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
226
+ const service = await payload.findByID({
227
+ id: serviceId,
228
+ collection: servicesSlug,
229
+ depth: 0,
230
+ req
231
+ });
232
+ if (service) {
233
+ result = {
234
+ after: service.bufferTimeAfter ?? 0,
235
+ before: service.bufferTimeBefore ?? 0
236
+ };
237
+ }
238
+ } catch {
239
+ // service missing — no buffers
240
+ }
241
+ }
242
+ bufferCache.set(key, result);
243
+ return result;
244
+ };
245
+ const fetchedOccupancies = (await Promise.all(docs.map((doc)=>reservationOccupancies({
246
+ bufferFor,
247
+ capacityMode,
248
+ reservation: doc,
249
+ resourceId
250
+ })))).flat();
251
+ // Sibling items from the same booking (review A5) — expanded with the same
252
+ // per-service buffers and capacity mode.
253
+ const siblingOccupancies = siblingItems ? await itemsToOccupancies({
254
+ bufferFor,
255
+ capacityMode,
256
+ items: siblingItems,
257
+ resourceId
258
+ }) : [];
259
+ const occupancies = [
260
+ ...fetchedOccupancies,
261
+ ...siblingOccupancies
262
+ ];
263
+ // Sum the units of every occupancy whose buffered window overlaps the candidate
264
+ const currentUnits = occupancies.reduce((sum, occ)=>doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd) ? sum + occ.units : sum, 0);
265
+ const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1;
266
+ const available = currentUnits + candidateUnits <= quantity;
146
267
  return {
147
- available: totalDocs + 1 <= quantity,
148
- currentCount: totalDocs,
149
- reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,
268
+ available,
269
+ currentCount: currentUnits,
270
+ reason: available ? undefined : capacityMode === 'per-guest' ? 'Guest capacity exceeded' : 'All units are booked for this time',
150
271
  totalCapacity: quantity
151
272
  };
152
273
  }
153
274
  export async function getAvailableSlots(params) {
154
- const { blockingStatuses, date, guestCount, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug } = params;
275
+ const { blockingStatuses, date, guestCount, payload, req, reservationSlug, resourceId, resourceIds, resourceSlug, scheduleSlug, serviceId, serviceSlug, timeZone } = params;
276
+ const tz = timeZone ?? 'UTC';
155
277
  // Resolve the set of resources to intersect (single-resource callers still work)
156
278
  const ids = resourceIds && resourceIds.length > 0 ? resourceIds : resourceId !== undefined ? [
157
279
  resourceId
@@ -200,9 +322,16 @@ export async function getAvailableSlots(params) {
200
322
  if (!schedules || schedules.length === 0) {
201
323
  continue;
202
324
  }
325
+ // A11: an exception on ANY of the resource's schedules makes the whole
326
+ // resource unavailable that day — not just the schedule it's recorded on.
327
+ const exceptedToday = schedules.some((s)=>isExceptionDate(date, s.exceptions ?? [], tz));
328
+ if (exceptedToday) {
329
+ scheduleBearingWindowLists.push([]);
330
+ continue;
331
+ }
203
332
  const windows = [];
204
333
  for (const schedule of schedules){
205
- windows.push(...resolveScheduleForDate(schedule, date));
334
+ windows.push(...resolveScheduleForDate(schedule, date, tz));
206
335
  }
207
336
  scheduleBearingWindowLists.push(windows);
208
337
  }
@@ -219,10 +348,14 @@ export async function getAvailableSlots(params) {
219
348
  return [];
220
349
  }
221
350
  // 4. Candidate slot sizing
351
+ // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.
352
+ // full-day services return early via the range-as-slot branch below and never
353
+ // consume slotDuration — keep it that way if reordering this function.
222
354
  const { endTime: slotEndOffset } = computeEndTime({
223
355
  durationType,
224
356
  serviceDuration: duration,
225
- startTime: new Date(0)
357
+ startTime: new Date(0),
358
+ timeZone: tz
226
359
  });
227
360
  const slotDuration = Math.round(slotEndOffset.getTime() / 60_000);
228
361
  const effectiveDuration = durationType === 'fixed' ? duration : slotDuration;
@@ -240,6 +373,7 @@ export async function getAvailableSlots(params) {
240
373
  reservationSlug,
241
374
  resourceId: rid,
242
375
  resourceSlug,
376
+ servicesSlug: serviceSlug,
243
377
  startTime: start
244
378
  });
245
379
  if (!result.available) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\n\nimport { resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport { addMinutes, computeBlockedWindow, intersectIntervals } from '../utilities/slotUtils.js'\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = new Date(startTime)\n end.setHours(23, 59, 59, 999)\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: number | string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Compute effective window with buffers\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Build overlap query\n const where = buildOverlapQuery({\n blockingStatuses,\n effectiveEnd,\n effectiveStart,\n excludeReservationId,\n resourceId,\n })\n\n if (capacityMode === 'per-guest') {\n // Must fetch docs to sum guestCount\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n select: { guestCount: true },\n where,\n })\n const currentGuests = docs.reduce(\n (sum: number, doc: Record<string, unknown>) => sum + ((doc.guestCount as number) ?? 1),\n 0,\n )\n return {\n available: currentGuests + guestCount <= quantity,\n currentCount: currentGuests,\n reason:\n currentGuests + guestCount > quantity ? 'Guest capacity exceeded' : undefined,\n totalCapacity: quantity,\n }\n }\n\n // per-reservation mode: count is sufficient\n // TODO: batch queries — linear per-item cost acceptable for 2-5 items\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { totalDocs } = await (payload.count as any)({\n collection: reservationSlug,\n req,\n where,\n })\n return {\n available: totalDocs + 1 <= quantity,\n currentCount: totalDocs,\n reason: totalDocs + 1 > quantity ? 'All units are booked for this time' : undefined,\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n } = params\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n if (ids.length === 0) {\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: {\n and: [{ resource: { equals: rid } }, { active: { equals: true } }],\n },\n })\n if (!schedules || schedules.length === 0) {\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n ),\n )\n }\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Candidate slot sizing\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n endTime: end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveScheduleForDate","addMinutes","computeBlockedWindow","intersectIntervals","computeEndTime","params","durationType","serviceDuration","startTime","end","Date","setHours","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","guestCount","payload","req","reservationSlug","resourceSlug","findByID","collection","depth","quantity","capacityMode","where","docs","find","limit","select","currentGuests","reduce","sum","doc","available","currentCount","undefined","totalCapacity","totalDocs","count","getAvailableSlots","date","resourceIds","scheduleSlug","serviceId","serviceSlug","ids","length","service","duration","bufferTimeBefore","bufferTimeAfter","scheduleBearingWindowLists","rid","schedules","active","windows","schedule","timeRanges","i","slotEndOffset","slotDuration","effectiveDuration","allAvailable","start","bBefore","bAfter","result","availableSlots","range","stepSize","min","candidateStart","candidateEnd"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/services/AvailabilityService.ts"],"sourcesContent":["import type { Payload, PayloadRequest, Where } from 'payload'\n\nimport type { CapacityMode, DurationType, StatusMachineConfig } from '../types.js'\nimport type { ResolvedItem } from '../utilities/resolveReservationItems.js'\n\nimport { resolveReservationItems } from '../utilities/resolveReservationItems.js'\nimport { isExceptionDate, resolveScheduleForDate } from '../utilities/scheduleUtils.js'\nimport {\n addMinutes,\n computeBlockedWindow,\n doRangesOverlap,\n intersectIntervals,\n} from '../utilities/slotUtils.js'\nimport { endOfDayInTimezone } from '../utilities/timezoneUtils.js'\n\n/** A window during which a resource is occupied, expanded by buffer times. */\nexport type Occupancy = { blockedEnd: Date; blockedStart: Date; units: number }\n\n/** Coarse pre-filter widen: covers any realistic neighbor buffer (buffers are\n * minutes). The precise per-item overlap check runs in memory afterwards. */\nconst COARSE_MARGIN_MS = 24 * 60 * 60 * 1000\n\n// --- Pure functions (no DB) ---\n\nexport function computeEndTime(params: {\n durationType: DurationType\n endTime?: Date\n serviceDuration: number\n startTime: Date\n timeZone?: string\n}): { durationMinutes: number; endTime: Date } {\n const { durationType, serviceDuration, startTime } = params\n\n if (durationType === 'full-day') {\n const end = endOfDayInTimezone(startTime, params.timeZone ?? 'UTC')\n const durationMinutes = Math.round((end.getTime() - startTime.getTime()) / 60_000)\n return { durationMinutes, endTime: end }\n }\n\n if (durationType === 'flexible' && params.endTime) {\n const durationMinutes = Math.round(\n (params.endTime.getTime() - startTime.getTime()) / 60_000,\n )\n return { durationMinutes, endTime: params.endTime }\n }\n\n // fixed duration (default)\n const endTime = addMinutes(startTime, serviceDuration)\n return { durationMinutes: serviceDuration, endTime }\n}\n\nexport function buildOverlapQuery(params: {\n blockingStatuses: string[]\n effectiveEnd: Date\n effectiveStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, effectiveEnd, effectiveStart, excludeReservationId, resourceId } =\n params\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: effectiveEnd.toISOString() } },\n { endTime: { greater_than: effectiveStart.toISOString() } },\n {\n or: [\n { resource: { equals: resourceId } },\n { 'items.resource': { equals: resourceId } },\n ],\n },\n ]\n\n if (excludeReservationId) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * Coarse superset query: blocking reservations whose top-level (span) window\n * comes within COARSE_MARGIN_MS of the candidate window and reference the\n * resource at top level or in items[]. The precise per-item overlap is computed\n * in memory afterwards — the top-level span is a superset of every item's\n * window, so this never misses a real conflict (margin covers neighbor buffers).\n */\nexport function buildCoarseOverlapQuery(params: {\n blockingStatuses: string[]\n candidateEnd: Date\n candidateStart: Date\n excludeReservationId?: number | string\n resourceId: number | string\n}): Where {\n const { blockingStatuses, candidateEnd, candidateStart, excludeReservationId, resourceId } =\n params\n const windowStart = new Date(candidateStart.getTime() - COARSE_MARGIN_MS)\n const windowEnd = new Date(candidateEnd.getTime() + COARSE_MARGIN_MS)\n\n const conditions: Where[] = [\n { status: { in: blockingStatuses } },\n { startTime: { less_than: windowEnd.toISOString() } },\n { endTime: { greater_than: windowStart.toISOString() } },\n {\n or: [{ resource: { equals: resourceId } }, { 'items.resource': { equals: resourceId } }],\n },\n ]\n\n if (excludeReservationId !== undefined) {\n conditions.push({ id: { not_equals: excludeReservationId } })\n }\n\n return { and: conditions }\n}\n\n/**\n * The occupancy windows a set of resolved items imposes on `resourceId`. Each\n * matching item's [startTime, endTime) is expanded by that item's own service\n * buffers (so neighbor buffers are enforced — review A3), and only the items\n * that actually reference `resourceId` count (so a multi-resource booking blocks\n * each resource only for its own item's window — review A4).\n */\nexport async function itemsToOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n items: ResolvedItem[]\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, items, resourceId } = params\n const occupancies: Occupancy[] = []\n\n for (const item of items) {\n if (String(item.resource) !== String(resourceId) || !item.endTime) {\n continue\n }\n const { after, before } = await bufferFor(item.service)\n const { effectiveEnd, effectiveStart } = computeBlockedWindow(\n new Date(item.startTime),\n new Date(item.endTime),\n before,\n after,\n )\n occupancies.push({\n blockedEnd: effectiveEnd,\n blockedStart: effectiveStart,\n units: capacityMode === 'per-guest' ? item.guestCount : 1,\n })\n }\n\n return occupancies\n}\n\n/** Occupancy a single fetched reservation imposes on `resourceId`. */\nexport async function reservationOccupancies(params: {\n bufferFor: (serviceId: number | string | undefined) => Promise<{ after: number; before: number }>\n capacityMode: CapacityMode\n reservation: Record<string, unknown>\n resourceId: number | string\n}): Promise<Occupancy[]> {\n const { bufferFor, capacityMode, reservation, resourceId } = params\n return itemsToOccupancies({\n bufferFor,\n capacityMode,\n items: resolveReservationItems(reservation),\n resourceId,\n })\n}\n\nexport function isBlockingStatus(\n status: string,\n statusMachine: StatusMachineConfig,\n): boolean {\n return statusMachine.blockingStatuses.includes(status)\n}\n\nexport function validateTransition(\n fromStatus: string,\n toStatus: string,\n statusMachine: StatusMachineConfig,\n): { reason?: string; valid: boolean } {\n const allowed = statusMachine.transitions[fromStatus]\n if (!allowed) {\n return { reason: `Unknown status: ${fromStatus}`, valid: false }\n }\n if (!allowed.includes(toStatus)) {\n return {\n reason: `Cannot transition from \"${fromStatus}\" to \"${toStatus}\"`,\n valid: false,\n }\n }\n return { valid: true }\n}\n\n// --- DB functions (use Payload Local API only) ---\n\nexport async function checkAvailability(params: {\n blockingStatuses: string[]\n bufferAfter: number\n bufferBefore: number\n endTime: Date\n excludeReservationId?: number | string\n guestCount: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId: number | string\n resourceSlug: string\n servicesSlug: string\n /** Other items from the same booking — counted as occupancy (review A5). */\n siblingItems?: ResolvedItem[]\n startTime: Date\n}): Promise<{\n available: boolean\n currentCount: number\n reason?: string\n totalCapacity: number\n}> {\n const {\n blockingStatuses,\n bufferAfter,\n bufferBefore,\n endTime,\n excludeReservationId,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceSlug,\n servicesSlug,\n siblingItems,\n startTime,\n } = params\n\n // Fetch resource for quantity and capacity mode\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resource = await (payload.findByID as any)({\n id: resourceId,\n collection: resourceSlug,\n depth: 0,\n req,\n })\n const quantity = (resource.quantity as number) ?? 1\n const capacityMode = ((resource.capacityMode as string) ?? 'per-reservation') as CapacityMode\n\n // Candidate window expanded by its own buffers\n const { effectiveEnd: candidateEnd, effectiveStart: candidateStart } = computeBlockedWindow(\n startTime,\n endTime,\n bufferBefore,\n bufferAfter,\n )\n\n // Coarse superset fetch — precise per-item overlap is computed in memory below\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs } = await (payload.find as any)({\n collection: reservationSlug,\n depth: 0,\n limit: 0,\n req,\n where: buildCoarseOverlapQuery({\n blockingStatuses,\n candidateEnd,\n candidateStart,\n excludeReservationId,\n resourceId,\n }),\n })\n\n // Per-call cache: fetch each distinct service's buffers at most once\n const bufferCache = new Map<string, { after: number; before: number }>()\n const bufferFor = async (\n serviceId: number | string | undefined,\n ): Promise<{ after: number; before: number }> => {\n const key = serviceId === undefined ? '' : String(serviceId)\n const cached = bufferCache.get(key)\n if (cached) {\n return cached\n }\n let result = { after: 0, before: 0 }\n if (serviceId !== undefined) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: servicesSlug,\n depth: 0,\n req,\n })\n if (service) {\n result = {\n after: (service.bufferTimeAfter as number) ?? 0,\n before: (service.bufferTimeBefore as number) ?? 0,\n }\n }\n } catch {\n // service missing — no buffers\n }\n }\n bufferCache.set(key, result)\n return result\n }\n\n const fetchedOccupancies = (\n await Promise.all(\n (docs as Array<Record<string, unknown>>).map((doc) =>\n reservationOccupancies({ bufferFor, capacityMode, reservation: doc, resourceId }),\n ),\n )\n ).flat()\n\n // Sibling items from the same booking (review A5) — expanded with the same\n // per-service buffers and capacity mode.\n const siblingOccupancies = siblingItems\n ? await itemsToOccupancies({ bufferFor, capacityMode, items: siblingItems, resourceId })\n : []\n\n const occupancies = [...fetchedOccupancies, ...siblingOccupancies]\n\n // Sum the units of every occupancy whose buffered window overlaps the candidate\n const currentUnits = occupancies.reduce(\n (sum, occ) =>\n doRangesOverlap(candidateStart, candidateEnd, occ.blockedStart, occ.blockedEnd)\n ? sum + occ.units\n : sum,\n 0,\n )\n\n const candidateUnits = capacityMode === 'per-guest' ? guestCount : 1\n const available = currentUnits + candidateUnits <= quantity\n\n return {\n available,\n currentCount: currentUnits,\n reason: available\n ? undefined\n : capacityMode === 'per-guest'\n ? 'Guest capacity exceeded'\n : 'All units are booked for this time',\n totalCapacity: quantity,\n }\n}\n\nexport async function getAvailableSlots(params: {\n blockingStatuses: string[]\n date: Date | string\n guestCount?: number\n payload: Payload\n req: PayloadRequest\n reservationSlug: string\n resourceId?: number | string\n resourceIds?: Array<number | string>\n resourceSlug: string\n scheduleSlug: string\n serviceId: number | string\n serviceSlug: string\n timeZone?: string\n}): Promise<Array<{ end: Date; start: Date }>> {\n const {\n blockingStatuses,\n date,\n guestCount,\n payload,\n req,\n reservationSlug,\n resourceId,\n resourceIds,\n resourceSlug,\n scheduleSlug,\n serviceId,\n serviceSlug,\n timeZone,\n } = params\n\n const tz = timeZone ?? 'UTC'\n\n // Resolve the set of resources to intersect (single-resource callers still work)\n const ids =\n resourceIds && resourceIds.length > 0\n ? resourceIds\n : resourceId !== undefined\n ? [resourceId]\n : []\n if (ids.length === 0) {\n return []\n }\n\n // 1. Service for duration + buffer times (from the primary service)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (payload.findByID as any)({\n id: serviceId,\n collection: serviceSlug,\n depth: 0,\n req,\n })\n const duration = (service.duration as number) ?? 60\n const bufferBefore = (service.bufferTimeBefore as number) ?? 0\n const bufferAfter = (service.bufferTimeAfter as number) ?? 0\n const durationType = ((service.durationType as string) ?? 'fixed') as DurationType\n\n // 2. Per resource: fetch schedules and resolve to windows. A resource with >=1\n // schedule is \"schedule-bearing\" and constrains time; a resource with zero\n // schedules is capacity-only and contributes no time windows.\n const scheduleBearingWindowLists: Array<Array<{ end: Date; start: Date }>> = []\n for (const rid of ids) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const { docs: schedules } = await (payload.find as any)({\n collection: scheduleSlug,\n depth: 0,\n limit: 100,\n req,\n where: {\n and: [{ resource: { equals: rid } }, { active: { equals: true } }],\n },\n })\n if (!schedules || schedules.length === 0) {\n continue\n }\n // A11: an exception on ANY of the resource's schedules makes the whole\n // resource unavailable that day — not just the schedule it's recorded on.\n const exceptedToday = (schedules as Array<Record<string, unknown>>).some((s) =>\n isExceptionDate(\n date,\n (s.exceptions as Array<{ date: string; endDate?: string }> | undefined) ?? [],\n tz,\n ),\n )\n if (exceptedToday) {\n scheduleBearingWindowLists.push([])\n continue\n }\n const windows: Array<{ end: Date; start: Date }> = []\n for (const schedule of schedules) {\n windows.push(\n ...resolveScheduleForDate(\n schedule as unknown as Parameters<typeof resolveScheduleForDate>[0],\n date,\n tz,\n ),\n )\n }\n scheduleBearingWindowLists.push(windows)\n }\n\n // No resource constrains time → no basis for generating slots\n if (scheduleBearingWindowLists.length === 0) {\n return []\n }\n\n // 3. Intersect all schedule-bearing window lists\n let timeRanges = scheduleBearingWindowLists[0]\n for (let i = 1; i < scheduleBearingWindowLists.length; i++) {\n timeRanges = intersectIntervals(timeRanges, scheduleBearingWindowLists[i])\n }\n if (timeRanges.length === 0) {\n return []\n }\n\n // 4. Candidate slot sizing\n // NOTE: epoch-trick sizing is only meaningful for fixed/flexible durations.\n // full-day services return early via the range-as-slot branch below and never\n // consume slotDuration — keep it that way if reordering this function.\n const { endTime: slotEndOffset } = computeEndTime({\n durationType,\n serviceDuration: duration,\n startTime: new Date(0),\n timeZone: tz,\n })\n const slotDuration = Math.round(slotEndOffset.getTime() / 60_000)\n const effectiveDuration = durationType === 'fixed' ? duration : slotDuration\n\n // Helper: a window is available only if EVERY required resource is free\n const allAvailable = async (\n start: Date,\n end: Date,\n bBefore: number,\n bAfter: number,\n ): Promise<boolean> => {\n for (const rid of ids) {\n const result = await checkAvailability({\n blockingStatuses,\n bufferAfter: bAfter,\n bufferBefore: bBefore,\n endTime: end,\n guestCount: guestCount ?? 1,\n payload,\n req,\n reservationSlug,\n resourceId: rid,\n resourceSlug,\n servicesSlug: serviceSlug,\n startTime: start,\n })\n if (!result.available) {\n return false\n }\n }\n return true\n }\n\n const availableSlots: Array<{ end: Date; start: Date }> = []\n\n // Full-day: offer each range as a single slot if all resources are free\n if (durationType === 'full-day') {\n for (const range of timeRanges) {\n if (await allAvailable(range.start, range.end, 0, 0)) {\n availableSlots.push({ end: range.end, start: range.start })\n }\n }\n return availableSlots\n }\n\n const stepSize = Math.min(effectiveDuration, 15)\n\n for (const range of timeRanges) {\n let candidateStart = new Date(range.start)\n\n while (true) {\n const candidateEnd = addMinutes(candidateStart, effectiveDuration)\n if (candidateEnd > range.end) {\n break\n }\n\n if (await allAvailable(candidateStart, candidateEnd, bufferBefore, bufferAfter)) {\n availableSlots.push({ end: candidateEnd, start: new Date(candidateStart) })\n }\n\n candidateStart = addMinutes(candidateStart, stepSize)\n }\n }\n\n return availableSlots\n}\n"],"names":["resolveReservationItems","isExceptionDate","resolveScheduleForDate","addMinutes","computeBlockedWindow","doRangesOverlap","intersectIntervals","endOfDayInTimezone","COARSE_MARGIN_MS","computeEndTime","params","durationType","serviceDuration","startTime","end","timeZone","durationMinutes","Math","round","getTime","endTime","buildOverlapQuery","blockingStatuses","effectiveEnd","effectiveStart","excludeReservationId","resourceId","conditions","status","in","less_than","toISOString","greater_than","or","resource","equals","push","id","not_equals","and","buildCoarseOverlapQuery","candidateEnd","candidateStart","windowStart","Date","windowEnd","undefined","itemsToOccupancies","bufferFor","capacityMode","items","occupancies","item","String","after","before","service","blockedEnd","blockedStart","units","guestCount","reservationOccupancies","reservation","isBlockingStatus","statusMachine","includes","validateTransition","fromStatus","toStatus","allowed","transitions","reason","valid","checkAvailability","bufferAfter","bufferBefore","payload","req","reservationSlug","resourceSlug","servicesSlug","siblingItems","findByID","collection","depth","quantity","docs","find","limit","where","bufferCache","Map","serviceId","key","cached","get","result","bufferTimeAfter","bufferTimeBefore","set","fetchedOccupancies","Promise","all","map","doc","flat","siblingOccupancies","currentUnits","reduce","sum","occ","candidateUnits","available","currentCount","totalCapacity","getAvailableSlots","date","resourceIds","scheduleSlug","serviceSlug","tz","ids","length","duration","scheduleBearingWindowLists","rid","schedules","active","exceptedToday","some","s","exceptions","windows","schedule","timeRanges","i","slotEndOffset","slotDuration","effectiveDuration","allAvailable","start","bBefore","bAfter","availableSlots","range","stepSize","min"],"mappings":"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"}
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "إنشاء عميل جديد",
100
100
  "fieldCustomerClear": "مسح التحديد",
101
101
  "calendarPending": "قيد الانتظار",
102
+ "calendarShowingNofM": "عرض {{shown}} من {{total}} حجزًا — قم بتضييق النطاق أو التصفية لرؤية الباقي.",
102
103
  "pendingDateTime": "التاريخ / الوقت",
103
104
  "pendingActions": "الإجراءات",
104
105
  "pendingSelectAll": "تحديد الكل",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Neuen Kunden anlegen",
100
100
  "fieldCustomerClear": "Auswahl aufheben",
101
101
  "calendarPending": "Ausstehend",
102
+ "calendarShowingNofM": "{{shown}} von {{total}} Reservierungen werden angezeigt – Zeitraum eingrenzen oder filtern, um den Rest zu sehen.",
102
103
  "pendingDateTime": "Datum / Zeit",
103
104
  "pendingActions": "Aktionen",
104
105
  "pendingSelectAll": "Alle auswählen",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Create new customer",
100
100
  "fieldCustomerClear": "Clear selection",
101
101
  "calendarPending": "Pending",
102
+ "calendarShowingNofM": "Showing {{shown}} of {{total}} reservations — narrow the range or filter to see the rest.",
102
103
  "pendingDateTime": "Date / Time",
103
104
  "pendingActions": "Actions",
104
105
  "pendingSelectAll": "Select all",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Crear nuevo cliente",
100
100
  "fieldCustomerClear": "Borrar selección",
101
101
  "calendarPending": "Pendiente",
102
+ "calendarShowingNofM": "Mostrando {{shown}} de {{total}} reservas: reduce el rango o filtra para ver el resto.",
102
103
  "pendingDateTime": "Fecha / Hora",
103
104
  "pendingActions": "Acciones",
104
105
  "pendingSelectAll": "Seleccionar todo",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "ایجاد مشتری جدید",
100
100
  "fieldCustomerClear": "پاک کردن انتخاب",
101
101
  "calendarPending": "در انتظار",
102
+ "calendarShowingNofM": "نمایش {{shown}} از {{total}} رزرو — برای دیدن بقیه بازه را محدود یا فیلتر کنید.",
102
103
  "pendingDateTime": "تاریخ / زمان",
103
104
  "pendingActions": "اقدامات",
104
105
  "pendingSelectAll": "انتخاب همه",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Créer un nouveau client",
100
100
  "fieldCustomerClear": "Effacer la sélection",
101
101
  "calendarPending": "En attente",
102
+ "calendarShowingNofM": "Affichage de {{shown}} sur {{total}} réservations — réduisez la plage ou filtrez pour voir le reste.",
102
103
  "pendingDateTime": "Date / Heure",
103
104
  "pendingActions": "Actions",
104
105
  "pendingSelectAll": "Tout sélectionner",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "नया ग्राहक बनाएँ",
100
100
  "fieldCustomerClear": "चयन हटाएँ",
101
101
  "calendarPending": "लंबित",
102
+ "calendarShowingNofM": "{{total}} में से {{shown}} आरक्षण दिखाए जा रहे हैं — बाकी देखने के लिए सीमा कम करें या फ़िल्टर करें।",
102
103
  "pendingDateTime": "तारीख / समय",
103
104
  "pendingActions": "क्रियाएँ",
104
105
  "pendingSelectAll": "सभी चुनें",
@@ -99,6 +99,7 @@
99
99
  "fieldCustomerCreateNew": "Buat pelanggan baru",
100
100
  "fieldCustomerClear": "Hapus pilihan",
101
101
  "calendarPending": "Menunggu",
102
+ "calendarShowingNofM": "Menampilkan {{shown}} dari {{total}} reservasi — persempit rentang atau filter untuk melihat sisanya.",
102
103
  "pendingDateTime": "Tanggal / Waktu",
103
104
  "pendingActions": "Tindakan",
104
105
  "pendingSelectAll": "Pilih semua",