@aglyn/plugins-commerce 1.0.0-beta.277 → 1.0.0-beta.282

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 (212) hide show
  1. package/package.json +17 -14
  2. package/src/lib/components/commerce-console-page.js +33 -1
  3. package/src/lib/components/commerce-console-page.js.map +1 -1
  4. package/src/lib/components/console/payments-settings-card.component.js +1 -1
  5. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  6. package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
  7. package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
  8. package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
  9. package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
  10. package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
  11. package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
  12. package/src/lib/components/console/pos-devices-card.component.js +5 -0
  13. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  14. package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
  15. package/src/lib/components/console/pos-hardware-card.component.js +456 -0
  16. package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
  17. package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
  18. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
  19. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
  20. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
  21. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
  22. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
  23. package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
  24. package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
  25. package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
  26. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
  27. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
  28. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
  29. package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
  30. package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
  31. package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
  32. package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
  33. package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
  34. package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
  35. package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
  36. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
  37. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
  38. package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
  39. package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
  40. package/src/lib/components/console/pos-page.component.js +6 -3
  41. package/src/lib/components/console/pos-page.component.js.map +1 -1
  42. package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
  43. package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
  44. package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
  45. package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
  46. package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
  47. package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
  48. package/src/lib/components/console/pos-zones.d.ts +37 -3
  49. package/src/lib/components/console/pos-zones.js +11 -1
  50. package/src/lib/components/console/pos-zones.js.map +1 -1
  51. package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
  52. package/src/lib/components/console/product-check-in-fields.component.js +172 -0
  53. package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
  54. package/src/lib/components/console/product-editor-dialog.component.js +48 -9
  55. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  56. package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
  57. package/src/lib/components/console/product-register-fields.component.js +39 -1
  58. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  59. package/src/lib/components/console/products-hub-card.component.js +21 -6
  60. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  61. package/src/lib/components/console/reservations-card.component.js +239 -70
  62. package/src/lib/components/console/reservations-card.component.js.map +1 -1
  63. package/src/lib/components/reservation-widget.js +16 -0
  64. package/src/lib/components/reservation-widget.js.map +1 -1
  65. package/src/lib/constants/operator-alerts.d.ts +6 -0
  66. package/src/lib/constants/operator-alerts.js +20 -1
  67. package/src/lib/constants/operator-alerts.js.map +1 -1
  68. package/src/lib/declarations.console-server.js +7 -0
  69. package/src/lib/declarations.console-server.js.map +1 -1
  70. package/src/lib/declarations.d.ts +26 -0
  71. package/src/lib/declarations.js +34 -0
  72. package/src/lib/declarations.js.map +1 -0
  73. package/src/lib/model/commerce-check-in-product.d.ts +37 -0
  74. package/src/lib/model/commerce-check-in-product.js +49 -0
  75. package/src/lib/model/commerce-check-in-product.js.map +1 -0
  76. package/src/lib/model/commerce-check-in.d.ts +187 -0
  77. package/src/lib/model/commerce-check-in.js +468 -0
  78. package/src/lib/model/commerce-check-in.js.map +1 -0
  79. package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
  80. package/src/lib/model/commerce-pos-day-report.js +572 -0
  81. package/src/lib/model/commerce-pos-day-report.js.map +1 -0
  82. package/src/lib/model/commerce-pos-offline.d.ts +65 -3
  83. package/src/lib/model/commerce-pos-offline.js +104 -7
  84. package/src/lib/model/commerce-pos-offline.js.map +1 -1
  85. package/src/lib/model/commerce-pos-ops.d.ts +3 -0
  86. package/src/lib/model/commerce-pos-ops.js +1 -0
  87. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  88. package/src/lib/model/commerce-pos.d.ts +43 -3
  89. package/src/lib/model/commerce-pos.js +73 -5
  90. package/src/lib/model/commerce-pos.js.map +1 -1
  91. package/src/lib/model/commerce-reservations.d.ts +73 -7
  92. package/src/lib/model/commerce-reservations.js +90 -10
  93. package/src/lib/model/commerce-reservations.js.map +1 -1
  94. package/src/lib/model/commerce.d.ts +40 -0
  95. package/src/lib/model/commerce.js +22 -4
  96. package/src/lib/model/commerce.js.map +1 -1
  97. package/src/lib/model/index.d.ts +1 -0
  98. package/src/lib/model/index.js +1 -0
  99. package/src/lib/model/index.js.map +1 -1
  100. package/src/lib/model/plugin-permissions.d.ts +1 -1
  101. package/src/lib/model/plugin-permissions.js +2 -1
  102. package/src/lib/model/plugin-permissions.js.map +1 -1
  103. package/src/lib/model/pos-hardware.d.ts +288 -0
  104. package/src/lib/model/pos-hardware.js +499 -0
  105. package/src/lib/model/pos-hardware.js.map +1 -0
  106. package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
  107. package/src/lib/model/pos-offline-card-limits.js +28 -0
  108. package/src/lib/model/pos-offline-card-limits.js.map +1 -0
  109. package/src/lib/model/pos-reader-splash.d.ts +114 -0
  110. package/src/lib/model/pos-reader-splash.js +141 -0
  111. package/src/lib/model/pos-reader-splash.js.map +1 -0
  112. package/src/lib/model/product-write.d.ts +6 -0
  113. package/src/lib/plugin-config.d.ts +10 -0
  114. package/src/lib/plugin-config.js +40 -0
  115. package/src/lib/plugin-config.js.map +1 -1
  116. package/src/lib/plugin.js +28 -0
  117. package/src/lib/plugin.js.map +1 -1
  118. package/src/lib/server/cart-checkout.js +3 -1
  119. package/src/lib/server/cart-checkout.js.map +1 -1
  120. package/src/lib/server/cart.js +12 -1
  121. package/src/lib/server/cart.js.map +1 -1
  122. package/src/lib/server/catalog.js +49 -12
  123. package/src/lib/server/catalog.js.map +1 -1
  124. package/src/lib/server/checkout-extras.js +1 -1
  125. package/src/lib/server/checkout-extras.js.map +1 -1
  126. package/src/lib/server/checkout.js +7 -0
  127. package/src/lib/server/checkout.js.map +1 -1
  128. package/src/lib/server/person-eraser.d.ts +8 -1
  129. package/src/lib/server/person-eraser.js +23 -1
  130. package/src/lib/server/person-eraser.js.map +1 -1
  131. package/src/lib/server/pos-check-in.d.ts +49 -0
  132. package/src/lib/server/pos-check-in.js +652 -0
  133. package/src/lib/server/pos-check-in.js.map +1 -0
  134. package/src/lib/server/pos-day-report.d.ts +46 -0
  135. package/src/lib/server/pos-day-report.js +299 -0
  136. package/src/lib/server/pos-day-report.js.map +1 -0
  137. package/src/lib/server/pos-hardware-admin.d.ts +50 -0
  138. package/src/lib/server/pos-hardware-admin.js +366 -0
  139. package/src/lib/server/pos-hardware-admin.js.map +1 -0
  140. package/src/lib/server/pos-hardware-notify.d.ts +27 -0
  141. package/src/lib/server/pos-hardware-notify.js +142 -0
  142. package/src/lib/server/pos-hardware-notify.js.map +1 -0
  143. package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
  144. package/src/lib/server/pos-hardware-user-eraser.js +67 -0
  145. package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
  146. package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
  147. package/src/lib/server/pos-hardware-webhook.js +176 -0
  148. package/src/lib/server/pos-hardware-webhook.js.map +1 -0
  149. package/src/lib/server/pos-hardware.d.ts +58 -0
  150. package/src/lib/server/pos-hardware.js +234 -0
  151. package/src/lib/server/pos-hardware.js.map +1 -0
  152. package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
  153. package/src/lib/server/pos-offline-card-reconcile.js +58 -0
  154. package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
  155. package/src/lib/server/pos-offline-sync.d.ts +32 -0
  156. package/src/lib/server/pos-offline-sync.js +191 -15
  157. package/src/lib/server/pos-offline-sync.js.map +1 -1
  158. package/src/lib/server/pos-ops-routes.d.ts +4 -2
  159. package/src/lib/server/pos-ops-routes.js +14 -2
  160. package/src/lib/server/pos-ops-routes.js.map +1 -1
  161. package/src/lib/server/pos-order.js +39 -51
  162. package/src/lib/server/pos-order.js.map +1 -1
  163. package/src/lib/server/pos-payment.js +8 -4
  164. package/src/lib/server/pos-payment.js.map +1 -1
  165. package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
  166. package/src/lib/server/pos-reader-splash-admin.js +295 -0
  167. package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
  168. package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
  169. package/src/lib/server/pos-reader-splash-render.js +138 -0
  170. package/src/lib/server/pos-reader-splash-render.js.map +1 -0
  171. package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
  172. package/src/lib/server/pos-reader-splash-server.js +32 -0
  173. package/src/lib/server/pos-reader-splash-server.js.map +1 -0
  174. package/src/lib/server/pos-reader-splash.d.ts +105 -0
  175. package/src/lib/server/pos-reader-splash.js +548 -0
  176. package/src/lib/server/pos-reader-splash.js.map +1 -0
  177. package/src/lib/server/pos-sale.d.ts +2 -1
  178. package/src/lib/server/pos-sale.js +3 -1
  179. package/src/lib/server/pos-sale.js.map +1 -1
  180. package/src/lib/server/pos-terminal.d.ts +67 -4
  181. package/src/lib/server/pos-terminal.js +239 -32
  182. package/src/lib/server/pos-terminal.js.map +1 -1
  183. package/src/lib/server/process-restock.js +4 -0
  184. package/src/lib/server/process-restock.js.map +1 -1
  185. package/src/lib/server/product-card.js +2 -2
  186. package/src/lib/server/product-card.js.map +1 -1
  187. package/src/lib/server/product-catalog.d.ts +2 -0
  188. package/src/lib/server/product-catalog.js +11 -4
  189. package/src/lib/server/product-catalog.js.map +1 -1
  190. package/src/lib/server/product.js +3 -1
  191. package/src/lib/server/product.js.map +1 -1
  192. package/src/lib/server/related.js +8 -1
  193. package/src/lib/server/related.js.map +1 -1
  194. package/src/lib/server/reservation-availability-query.d.ts +35 -0
  195. package/src/lib/server/reservation-availability-query.js +34 -0
  196. package/src/lib/server/reservation-availability-query.js.map +1 -0
  197. package/src/lib/server/reservation-availability.d.ts +21 -0
  198. package/src/lib/server/reservation-availability.js +64 -10
  199. package/src/lib/server/reservation-availability.js.map +1 -1
  200. package/src/lib/server/reservation-walk-in.d.ts +28 -0
  201. package/src/lib/server/reservation-walk-in.js +166 -0
  202. package/src/lib/server/reservation-walk-in.js.map +1 -0
  203. package/src/lib/server/reserve.js +13 -10
  204. package/src/lib/server/reserve.js.map +1 -1
  205. package/src/lib/server/reviews.js +3 -1
  206. package/src/lib/server/reviews.js.map +1 -1
  207. package/src/lib/server/site-page-resolver.js +5 -2
  208. package/src/lib/server/site-page-resolver.js.map +1 -1
  209. package/src/lib/server.js +26 -0
  210. package/src/lib/server.js.map +1 -1
  211. package/src/lib/testing/fake-firestore.js +72 -0
  212. package/src/lib/testing/fake-firestore.js.map +1 -1
@@ -25,7 +25,8 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
25
25
  import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Typography } from "@mui/material";
26
26
  import { collection, deleteDoc, doc, getDocs, limit, orderBy, query, setDoc, updateDoc, where } from "firebase/firestore";
27
27
  import { useCallback, useMemo, useState } from "react";
28
- import { useFirestore, useMerchantPayments } from "@aglyn/tenant-feature-instance";
28
+ import { useFirestore, useMerchantPayments, useUser } from "@aglyn/tenant-feature-instance";
29
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
29
30
  import { PrerequisiteNotice } from "@aglyn/shared-ui-jsx/components/prerequisite-notice.component";
30
31
  import { useFirestoreCollection, usePagedCollection, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
31
32
  import { pluginDocsHelp } from "@aglyn/aglyn";
@@ -39,19 +40,38 @@ const STATUS_COLOR = {
39
40
  };
40
41
  const usd = (cents)=>`$${((cents != null ? cents : 0) / 100).toFixed(2)}`;
41
42
  const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
43
+ const nightLabel = (dayMs)=>new Date(dayMs).toLocaleDateString('en-US', {
44
+ weekday: 'short',
45
+ month: 'short',
46
+ day: 'numeric',
47
+ timeZone: 'UTC'
48
+ });
49
+ /** The Reservations card's look-ahead for a several-unit resource. */ const UNITS_LOOKAHEAD_NIGHTS = 14;
50
+ /** The desk's calendar day as the UTC-midnight `dayMs` stays are stored in. */ function localTodayDayMs() {
51
+ const now = new Date();
52
+ return Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
53
+ }
54
+ /** The units field's problem, or null when the typed value is a valid count. */ function unitsProblem(units) {
55
+ if (units === undefined) return null;
56
+ if (!Number.isInteger(units) || units < 1 || units > CommerceModel.MAX_RESOURCE_UNITS) {
57
+ return `A whole number from 1 to ${CommerceModel.MAX_RESOURCE_UNITS}`;
58
+ }
59
+ return null;
60
+ }
42
61
  /**
43
62
  * Reservations console (AGL-311): resources CRUD (nightly pricing,
44
63
  * seasons via multipliers, deposits) plus the reservations list with
45
64
  * check-in/out, no-show, cancel, and walk-in creation. The storefront
46
65
  * widget books against the same docs.
47
66
  */ export function ReservationsCard(props) {
48
- var _payments_href, _payments_reason, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11;
67
+ var _payments_href, _payments_reason, _ref, _ref1, _ref2, _unitsProblem, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12, _ref13;
49
68
  var _resourceDraft_name;
50
69
  const { hostId } = props;
51
70
  // Every reservation is paid when it is booked, so a store that cannot
52
71
  // charge turns each guest away; the card says so up front.
53
72
  const payments = useMerchantPayments(hostId);
54
73
  const firestore = useFirestore();
74
+ const { data: user } = useUser();
55
75
  const { enqueueSnackbar } = useSnackbar();
56
76
  const { confirm } = useConfirmationContext();
57
77
  const { data: resourceDocs, status: resourcesStatus, /**
@@ -82,6 +102,50 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
82
102
  ], {
83
103
  idField: '$id'
84
104
  });
105
+ /*
106
+ * Units left on each of the next nights, for resources with several units
107
+ * (AGL-4044). One listen across the site's resources, on the stays that
108
+ * have not ended, nearest check-out first: a range and an order on the same
109
+ * field, which Firestore's automatic single-field index serves. Not read at
110
+ * all while every resource has one unit, so a cabin-only site pays nothing.
111
+ *
112
+ * The 500 cap is the booking door's: it can only leave out stays that end
113
+ * after 500 others that are all still running, which would be a stay far
114
+ * past the two weeks this draws.
115
+ */ const hasSeveralUnits = (resourceDocs != null ? resourceDocs : []).some((resource)=>CommerceModel.resourceUnits(resource) > 1);
116
+ const todayDayMs = localTodayDayMs();
117
+ const { data: currentStays } = useFirestoreCollection(()=>hasSeveralUnits ? query(collection(firestore, 'hosts', hostId, 'reservations'), where('checkOutDayMs', '>', todayDayMs), orderBy('checkOutDayMs'), limit(500)) : null, [
118
+ firestore,
119
+ hostId,
120
+ hasSeveralUnits,
121
+ todayDayMs
122
+ ], {
123
+ idField: '$id'
124
+ });
125
+ const unitsAhead = useMemo(()=>{
126
+ const byResource = {};
127
+ if (!hasSeveralUnits) return byResource;
128
+ const nowMs = Date.now();
129
+ for (const resource of resourceDocs != null ? resourceDocs : []){
130
+ if (CommerceModel.resourceUnits(resource) === 1) continue;
131
+ const stays = (currentStays != null ? currentStays : []).filter((stay)=>stay.resourceId === resource.$id).map((stay)=>{
132
+ var _stay_createdAtMs;
133
+ return {
134
+ checkInDayMs: Number(stay.checkInDayMs),
135
+ checkOutDayMs: Number(stay.checkOutDayMs),
136
+ status: stay.status,
137
+ createdAtMs: Number((_stay_createdAtMs = stay.createdAtMs) != null ? _stay_createdAtMs : 0)
138
+ };
139
+ });
140
+ byResource[resource.$id] = CommerceModel.unitsLeftByNight(resource, stays, todayDayMs, UNITS_LOOKAHEAD_NIGHTS, nowMs);
141
+ }
142
+ return byResource;
143
+ }, [
144
+ hasSeveralUnits,
145
+ resourceDocs,
146
+ currentStays,
147
+ todayDayMs
148
+ ]);
85
149
  const resourceNames = useMemo(()=>{
86
150
  const map = {};
87
151
  for (const resource of resourceDocs != null ? resourceDocs : []){
@@ -93,6 +157,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
93
157
  ]);
94
158
  const [resourceDraft, setResourceDraft] = useState(null);
95
159
  const [walkIn, setWalkIn] = useState(null);
160
+ const [walkInBusy, setWalkInBusy] = useState(false);
96
161
  const handleResourceSave = useCallback(async ()=>{
97
162
  var _resourceDraft_name;
98
163
  if (!(resourceDraft == null ? void 0 : (_resourceDraft_name = resourceDraft.name) == null ? void 0 : _resourceDraft_name.trim()) || !(resourceDraft.nightlyRateUsd > 0)) {
@@ -108,11 +173,21 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
108
173
  * it into the stored document as a real field. Nothing reads it; it is
109
174
  * the listener's bookkeeping leaking into storage, and once there no
110
175
  * reader can tell it from a field the card meant to write.
111
- */ const seeded = resourceDraft;
112
- const { id, $id: _syntheticId } = seeded, data = _object_without_properties_loose(seeded, [
176
+ */ if (unitsProblem(resourceDraft.units)) return;
177
+ const seeded = resourceDraft;
178
+ const { id, $id: _syntheticId, units } = seeded, fields = _object_without_properties_loose(seeded, [
113
179
  "id",
114
- "$id"
180
+ "$id",
181
+ "units"
115
182
  ]);
183
+ // Units are written as the count the model reads (AGL-4044), and a
184
+ // cleared field as 1 rather than left out: `merge: true` would otherwise
185
+ // keep a stored 12 the merchant just removed.
186
+ const data = 'units' in seeded ? _extends({}, fields, {
187
+ units: CommerceModel.resourceUnits({
188
+ units
189
+ })
190
+ }) : fields;
116
191
  /**
117
192
  * Refuse an EDIT whose seed the server never confirmed (AGL-1358).
118
193
  * `data` is the whole stored row spread out of the listener, so
@@ -208,25 +283,53 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
208
283
  status: item.status,
209
284
  createdAtMs: item.createdAtMs
210
285
  }));
286
+ // Count-based for a several-unit resource (AGL-4044): the walk-in takes
287
+ // any unit that is free on every night, and is refused only when some
288
+ // night has none left.
211
289
  if (!CommerceModel.isRangeAvailable(resource, existing, checkInDayMs, checkOutDayMs)) {
212
- return void enqueueSnackbar('Those dates are taken', {
290
+ const message = CommerceModel.resourceUnits(resource) > 1 ? 'Every unit is taken on at least one of those nights' : 'Those dates are taken';
291
+ return void enqueueSnackbar(message, {
213
292
  variant: 'warning',
214
293
  persist: false
215
294
  });
216
295
  }
217
- await setDoc(doc(firestore, 'hosts', hostId, 'reservations', Aglyn.createResourceUid()), {
218
- resourceId: walkIn.resourceId,
219
- status: 'confirmed',
220
- checkInDayMs,
221
- checkOutDayMs,
222
- guestName: walkIn.guestName || null,
223
- guestEmail: null,
224
- nights: quote.nights,
225
- totalCents: quote.totalCents,
226
- depositCents: 0,
227
- paidCents: 0,
228
- createdAtMs: Date.now()
229
- });
296
+ /*
297
+ * The check above is the instant answer; the WRITE goes through
298
+ * `/api/commerce/reservations/walk-in` (AGL-4046), which re-asks the same
299
+ * question inside the transaction that books the stay. A client
300
+ * read-then-write let two staff on two devices both take the last unit.
301
+ */ setWalkInBusy(true);
302
+ try {
303
+ const response = await authorizedFetch(user, '/api/commerce/reservations/walk-in', {
304
+ method: 'POST',
305
+ headers: {
306
+ 'Content-Type': 'application/json'
307
+ },
308
+ body: JSON.stringify({
309
+ hostId,
310
+ resourceId: walkIn.resourceId,
311
+ checkInDayMs,
312
+ checkOutDayMs,
313
+ guestName: walkIn.guestName || undefined,
314
+ reservationId: walkIn.attemptId
315
+ })
316
+ });
317
+ const answer = await response.json().catch(()=>({}));
318
+ if (!response.ok) {
319
+ var _ref;
320
+ return void enqueueSnackbar((_ref = answer == null ? void 0 : answer.error) != null ? _ref : 'Could not reserve the walk-in — try again', {
321
+ variant: response.status === 409 || response.status === 400 ? 'warning' : 'error',
322
+ persist: false
323
+ });
324
+ }
325
+ } catch (unused) {
326
+ return void enqueueSnackbar('Could not reserve the walk-in — try again', {
327
+ variant: 'error',
328
+ persist: false
329
+ });
330
+ } finally{
331
+ setWalkInBusy(false);
332
+ }
230
333
  setWalkIn(null);
231
334
  enqueueSnackbar('Walk-in reserved — collect payment at POS', {
232
335
  variant: 'success',
@@ -237,6 +340,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
237
340
  resourceDocs,
238
341
  firestore,
239
342
  hostId,
343
+ user,
240
344
  enqueueSnackbar
241
345
  ]);
242
346
  return /*#__PURE__*/ _jsxs(CardDisplay, {
@@ -261,55 +365,81 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
261
365
  children: 'Resources'
262
366
  }),
263
367
  (resourceDocs != null ? resourceDocs : []).map((resource)=>/*#__PURE__*/ _jsxs(Stack, {
264
- direction: "row",
265
- spacing: 1,
266
- sx: {
267
- alignItems: 'center'
268
- },
368
+ spacing: 0.5,
269
369
  children: [
270
370
  /*#__PURE__*/ _jsxs(Stack, {
371
+ direction: "row",
372
+ spacing: 1,
271
373
  sx: {
272
- flex: 1,
273
- minWidth: 0
374
+ alignItems: 'center'
274
375
  },
275
376
  children: [
276
- /*#__PURE__*/ _jsx(Typography, {
277
- variant: "body2",
278
- noWrap: true,
279
- children: `${resource.name} · $${resource.nightlyRateUsd}/night`
377
+ /*#__PURE__*/ _jsxs(Stack, {
378
+ sx: {
379
+ flex: 1,
380
+ minWidth: 0
381
+ },
382
+ children: [
383
+ /*#__PURE__*/ _jsx(Typography, {
384
+ variant: "body2",
385
+ noWrap: true,
386
+ children: [
387
+ resource.name,
388
+ `$${resource.nightlyRateUsd}/night`,
389
+ ...CommerceModel.resourceUnits(resource) > 1 ? [
390
+ `${CommerceModel.resourceUnits(resource)} units`
391
+ ] : []
392
+ ].join(' · ')
393
+ }),
394
+ /*#__PURE__*/ _jsx(Typography, {
395
+ variant: "caption",
396
+ color: "text.secondary",
397
+ noWrap: true,
398
+ children: `id: ${resource.$id}`
399
+ })
400
+ ]
280
401
  }),
281
- /*#__PURE__*/ _jsx(Typography, {
282
- variant: "caption",
283
- color: "text.secondary",
284
- noWrap: true,
285
- children: `id: ${resource.$id}`
402
+ /*#__PURE__*/ _jsx(Button, {
403
+ size: "small",
404
+ onClick: ()=>setResourceDraft(_extends({
405
+ id: resource.$id
406
+ }, resource)),
407
+ children: 'Edit'
408
+ }),
409
+ /*#__PURE__*/ _jsx(Button, {
410
+ size: "small",
411
+ color: "error",
412
+ onClick: async ()=>{
413
+ const confirmed = await confirm({
414
+ title: 'Remove this resource?',
415
+ description: 'Existing reservations keep their records; the widget ' + 'stops accepting new stays.',
416
+ confirmationText: 'Remove',
417
+ confirmationButtonProps: {
418
+ color: 'error'
419
+ }
420
+ }).then(()=>true).catch(()=>false);
421
+ if (!confirmed) return;
422
+ await deleteDoc(doc(firestore, 'hosts', hostId, 'resources', resource.$id));
423
+ },
424
+ children: 'Remove'
286
425
  })
287
426
  ]
288
427
  }),
289
- /*#__PURE__*/ _jsx(Button, {
290
- size: "small",
291
- onClick: ()=>setResourceDraft(_extends({
292
- id: resource.$id
293
- }, resource)),
294
- children: 'Edit'
295
- }),
296
- /*#__PURE__*/ _jsx(Button, {
297
- size: "small",
298
- color: "error",
299
- onClick: async ()=>{
300
- const confirmed = await confirm({
301
- title: 'Remove this resource?',
302
- description: 'Existing reservations keep their records; the widget ' + 'stops accepting new stays.',
303
- confirmationText: 'Remove',
304
- confirmationButtonProps: {
305
- color: 'error'
306
- }
307
- }).then(()=>true).catch(()=>false);
308
- if (!confirmed) return;
309
- await deleteDoc(doc(firestore, 'hosts', hostId, 'resources', resource.$id));
428
+ unitsAhead[resource.$id] ? /*#__PURE__*/ _jsx(Stack, {
429
+ direction: "row",
430
+ useFlexGap: true,
431
+ "aria-label": `Units left on the next ${UNITS_LOOKAHEAD_NIGHTS} nights`,
432
+ sx: {
433
+ flexWrap: 'wrap',
434
+ gap: 0.5
310
435
  },
311
- children: 'Remove'
312
- })
436
+ children: unitsAhead[resource.$id].map((night)=>/*#__PURE__*/ _jsx(Chip, {
437
+ size: "small",
438
+ variant: "outlined",
439
+ color: night.unitsLeft === 0 ? 'error' : night.unitsLeft <= CommerceModel.FEW_UNITS_LEFT ? 'warning' : 'default',
440
+ label: `${nightLabel(night.dayMs)} · ${night.unitsLeft} of ${night.units} left`
441
+ }, night.dayMs))
442
+ }) : null
313
443
  ]
314
444
  }, resource.$id)),
315
445
  /*#__PURE__*/ _jsxs(Stack, {
@@ -337,7 +467,8 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
337
467
  resourceId: (_ref = resourceDocs == null ? void 0 : (_resourceDocs_ = resourceDocs[0]) == null ? void 0 : _resourceDocs_.$id) != null ? _ref : '',
338
468
  checkIn: '',
339
469
  checkOut: '',
340
- guestName: ''
470
+ guestName: '',
471
+ attemptId: Aglyn.createResourceUid()
341
472
  });
342
473
  },
343
474
  children: 'Walk-in'
@@ -482,13 +613,51 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
482
613
  },
483
614
  placeholder: "Lakeside cabin"
484
615
  }),
616
+ /*#__PURE__*/ _jsxs(Stack, {
617
+ direction: "row",
618
+ spacing: 1,
619
+ children: [
620
+ /*#__PURE__*/ _jsx(TextField, {
621
+ label: "Spots or units available",
622
+ placeholder: "1",
623
+ value: (_ref2 = resourceDraft == null ? void 0 : resourceDraft.units) != null ? _ref2 : '',
624
+ onChange: (event)=>setResourceDraft((prev)=>prev ? _extends({}, prev, {
625
+ units: event.target.value.replace(/\D/g, '') === '' ? undefined : Number(event.target.value.replace(/\D/g, ''))
626
+ }) : prev),
627
+ error: Boolean(unitsProblem(resourceDraft == null ? void 0 : resourceDraft.units)),
628
+ helperText: (_unitsProblem = unitsProblem(resourceDraft == null ? void 0 : resourceDraft.units)) != null ? _unitsProblem : 'How many identical ones you have, like 12 RV hookups',
629
+ size: "small",
630
+ fullWidth: true,
631
+ slotProps: {
632
+ htmlInput: {
633
+ inputMode: 'numeric'
634
+ }
635
+ }
636
+ }),
637
+ /*#__PURE__*/ _jsx(TextField, {
638
+ label: "Sleeps",
639
+ value: (_ref3 = resourceDraft == null ? void 0 : resourceDraft.capacity) != null ? _ref3 : '',
640
+ onChange: (event)=>setResourceDraft((prev)=>prev ? _extends({}, prev, {
641
+ capacity: Math.round(Number(event.target.value)) || undefined
642
+ }) : prev),
643
+ helperText: "Guests per unit, for display",
644
+ size: "small",
645
+ fullWidth: true,
646
+ slotProps: {
647
+ htmlInput: {
648
+ inputMode: 'numeric'
649
+ }
650
+ }
651
+ })
652
+ ]
653
+ }),
485
654
  /*#__PURE__*/ _jsxs(Stack, {
486
655
  direction: "row",
487
656
  spacing: 1,
488
657
  children: [
489
658
  /*#__PURE__*/ _jsx(TextField, {
490
659
  label: "Nightly rate ($)",
491
- value: (_ref2 = resourceDraft == null ? void 0 : resourceDraft.nightlyRateUsd) != null ? _ref2 : '',
660
+ value: (_ref4 = resourceDraft == null ? void 0 : resourceDraft.nightlyRateUsd) != null ? _ref4 : '',
492
661
  onChange: (event)=>setResourceDraft((prev)=>prev ? _extends({}, prev, {
493
662
  nightlyRateUsd: Number(event.target.value) || 0
494
663
  }) : prev),
@@ -502,7 +671,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
502
671
  /*#__PURE__*/ _jsx(TextField, {
503
672
  label: "Weekend ×",
504
673
  placeholder: "1",
505
- value: (_ref3 = resourceDraft == null ? void 0 : resourceDraft.weekendMultiplier) != null ? _ref3 : '',
674
+ value: (_ref5 = resourceDraft == null ? void 0 : resourceDraft.weekendMultiplier) != null ? _ref5 : '',
506
675
  onChange: (event)=>setResourceDraft((prev)=>prev ? _extends({}, prev, {
507
676
  weekendMultiplier: Number(event.target.value) || undefined
508
677
  }) : prev),
@@ -521,7 +690,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
521
690
  children: [
522
691
  /*#__PURE__*/ _jsx(TextField, {
523
692
  label: "Min nights",
524
- value: (_ref4 = resourceDraft == null ? void 0 : resourceDraft.minNights) != null ? _ref4 : '',
693
+ value: (_ref6 = resourceDraft == null ? void 0 : resourceDraft.minNights) != null ? _ref6 : '',
525
694
  onChange: (event)=>setResourceDraft((prev)=>prev ? _extends({}, prev, {
526
695
  minNights: Math.round(Number(event.target.value)) || undefined
527
696
  }) : prev),
@@ -535,7 +704,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
535
704
  /*#__PURE__*/ _jsx(TextField, {
536
705
  label: "Deposit %",
537
706
  placeholder: "100 = pay in full",
538
- value: (_ref5 = resourceDraft == null ? void 0 : resourceDraft.depositPct) != null ? _ref5 : '',
707
+ value: (_ref7 = resourceDraft == null ? void 0 : resourceDraft.depositPct) != null ? _ref7 : '',
539
708
  onChange: (event)=>setResourceDraft((prev)=>prev ? _extends({}, prev, {
540
709
  depositPct: Math.round(Number(event.target.value)) || undefined
541
710
  }) : prev),
@@ -548,7 +717,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
548
717
  }),
549
718
  /*#__PURE__*/ _jsx(TextField, {
550
719
  label: "Free cancel (hrs)",
551
- value: (_ref6 = resourceDraft == null ? void 0 : resourceDraft.cancellationHours) != null ? _ref6 : '',
720
+ value: (_ref8 = resourceDraft == null ? void 0 : resourceDraft.cancellationHours) != null ? _ref8 : '',
552
721
  onChange: (event)=>setResourceDraft((prev)=>prev ? _extends({}, prev, {
553
722
  cancellationHours: Math.round(Number(event.target.value)) || undefined
554
723
  }) : prev),
@@ -572,7 +741,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
572
741
  /*#__PURE__*/ _jsx(Button, {
573
742
  variant: "contained",
574
743
  color: "primary",
575
- disabled: !(resourceDraft == null ? void 0 : (_resourceDraft_name = resourceDraft.name) == null ? void 0 : _resourceDraft_name.trim()) || !(((_ref7 = resourceDraft == null ? void 0 : resourceDraft.nightlyRateUsd) != null ? _ref7 : 0) > 0),
744
+ disabled: !(resourceDraft == null ? void 0 : (_resourceDraft_name = resourceDraft.name) == null ? void 0 : _resourceDraft_name.trim()) || !(((_ref9 = resourceDraft == null ? void 0 : resourceDraft.nightlyRateUsd) != null ? _ref9 : 0) > 0) || Boolean(unitsProblem(resourceDraft == null ? void 0 : resourceDraft.units)),
576
745
  onClick: handleResourceSave,
577
746
  children: 'Save'
578
747
  })
@@ -598,7 +767,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
598
767
  children: [
599
768
  /*#__PURE__*/ _jsx(TextField, {
600
769
  label: "Resource",
601
- value: (_ref8 = walkIn == null ? void 0 : walkIn.resourceId) != null ? _ref8 : '',
770
+ value: (_ref10 = walkIn == null ? void 0 : walkIn.resourceId) != null ? _ref10 : '',
602
771
  onChange: (event)=>setWalkIn((prev)=>prev ? _extends({}, prev, {
603
772
  resourceId: event.target.value
604
773
  }) : prev),
@@ -619,7 +788,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
619
788
  /*#__PURE__*/ _jsx(TextField, {
620
789
  label: "Check-in",
621
790
  type: "date",
622
- value: (_ref9 = walkIn == null ? void 0 : walkIn.checkIn) != null ? _ref9 : '',
791
+ value: (_ref11 = walkIn == null ? void 0 : walkIn.checkIn) != null ? _ref11 : '',
623
792
  onChange: (event)=>setWalkIn((prev)=>prev ? _extends({}, prev, {
624
793
  checkIn: event.target.value
625
794
  }) : prev),
@@ -634,7 +803,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
634
803
  /*#__PURE__*/ _jsx(TextField, {
635
804
  label: "Check-out",
636
805
  type: "date",
637
- value: (_ref10 = walkIn == null ? void 0 : walkIn.checkOut) != null ? _ref10 : '',
806
+ value: (_ref12 = walkIn == null ? void 0 : walkIn.checkOut) != null ? _ref12 : '',
638
807
  onChange: (event)=>setWalkIn((prev)=>prev ? _extends({}, prev, {
639
808
  checkOut: event.target.value
640
809
  }) : prev),
@@ -650,7 +819,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
650
819
  }),
651
820
  /*#__PURE__*/ _jsx(TextField, {
652
821
  label: "Guest name",
653
- value: (_ref11 = walkIn == null ? void 0 : walkIn.guestName) != null ? _ref11 : '',
822
+ value: (_ref13 = walkIn == null ? void 0 : walkIn.guestName) != null ? _ref13 : '',
654
823
  onChange: (event)=>setWalkIn((prev)=>prev ? _extends({}, prev, {
655
824
  guestName: event.target.value
656
825
  }) : prev),
@@ -667,7 +836,7 @@ const day = (dayMs)=>dayMs ? new Date(dayMs).toISOString().slice(0, 10) : '—';
667
836
  /*#__PURE__*/ _jsx(Button, {
668
837
  variant: "contained",
669
838
  color: "primary",
670
- disabled: !(walkIn == null ? void 0 : walkIn.resourceId) || !(walkIn == null ? void 0 : walkIn.checkIn) || !(walkIn == null ? void 0 : walkIn.checkOut),
839
+ disabled: walkInBusy || !(walkIn == null ? void 0 : walkIn.resourceId) || !(walkIn == null ? void 0 : walkIn.checkIn) || !(walkIn == null ? void 0 : walkIn.checkOut),
671
840
  onClick: handleWalkIn,
672
841
  children: 'Reserve'
673
842
  })