@burdenoff/microfe-ggnomad 2026.725.2 → 2026.726.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ggnomad/GGNomadRoot.d.ts.map +1 -1
- package/dist/ggnomad/GGNomadRoot.js +66 -52
- package/dist/ggnomad/GGNomadRoot.js.map +1 -1
- package/dist/ggnomad/GGNomadRoutes.d.ts.map +1 -1
- package/dist/ggnomad/GGNomadRoutes.js +13 -5
- package/dist/ggnomad/GGNomadRoutes.js.map +1 -1
- package/dist/ggnomad/components/AuditTrailDialog.d.ts +17 -0
- package/dist/ggnomad/components/AuditTrailDialog.d.ts.map +1 -0
- package/dist/ggnomad/components/AuditTrailDialog.js +88 -0
- package/dist/ggnomad/components/AuditTrailDialog.js.map +1 -0
- package/dist/ggnomad/components/CreateTravelPolicyDialog.d.ts +14 -0
- package/dist/ggnomad/components/CreateTravelPolicyDialog.d.ts.map +1 -0
- package/dist/ggnomad/components/CreateTravelPolicyDialog.js +192 -0
- package/dist/ggnomad/components/CreateTravelPolicyDialog.js.map +1 -0
- package/dist/ggnomad/components/index.d.ts +2 -0
- package/dist/ggnomad/components/index.d.ts.map +1 -1
- package/dist/ggnomad/components/index.js +2 -0
- package/dist/ggnomad/graphql.d.ts +20 -0
- package/dist/ggnomad/graphql.d.ts.map +1 -1
- package/dist/ggnomad/graphql.js +199 -1
- package/dist/ggnomad/graphql.js.map +1 -1
- package/dist/ggnomad/pages/OperationsQueuePage.d.ts +11 -0
- package/dist/ggnomad/pages/OperationsQueuePage.d.ts.map +1 -0
- package/dist/ggnomad/pages/OperationsQueuePage.js +323 -0
- package/dist/ggnomad/pages/OperationsQueuePage.js.map +1 -0
- package/dist/ggnomad/pages/TravelPolicyPage.d.ts +9 -0
- package/dist/ggnomad/pages/TravelPolicyPage.d.ts.map +1 -0
- package/dist/ggnomad/pages/TravelPolicyPage.js +245 -0
- package/dist/ggnomad/pages/TravelPolicyPage.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { APPROVE_GGNOMAD_LISTING_MUTATION as e, CANCEL_GGNOMAD_BOOKING_MUTATION as t, CHECK_IN_GGNOMAD_BOOKING_MUTATION as n, COMPLETE_GGNOMAD_BOOKING_MUTATION as r, GGNOMAD_BOOKING_QUEUE_QUERY as ee, GGNOMAD_LISTING_REVIEW_QUEUE_QUERY as te, MARK_GGNOMAD_BOOKING_NO_SHOW_MUTATION as ne, REJECT_GGNOMAD_LISTING_MUTATION as i } from "../graphql.js";
|
|
2
|
+
import { PageShell as a } from "./PageShell.js";
|
|
3
|
+
import { StatusBadge as o } from "../components/StatusBadge.js";
|
|
4
|
+
import { SectionCard as s } from "../components/SectionCard.js";
|
|
5
|
+
import { EntityCard as c } from "../components/EntityCard.js";
|
|
6
|
+
import { notify as l } from "../utils/notify.js";
|
|
7
|
+
import { AuditTrailDialog as re } from "../components/AuditTrailDialog.js";
|
|
8
|
+
import "../components/index.js";
|
|
9
|
+
import { useMemo as u, useState as d } from "react";
|
|
10
|
+
import { Check as f, ClipboardList as p, History as m, LogIn as h, LogOut as g, ShieldQuestion as _, UserX as ie, X as ae, XCircle as oe } from "lucide-react";
|
|
11
|
+
import { useI18n as se } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
12
|
+
import { useMutation as v, useQuery as y } from "@apollo/client/react";
|
|
13
|
+
import { AlertDialog as b, AlertDialogAction as x, AlertDialogCancel as S, AlertDialogContent as C, AlertDialogDescription as w, AlertDialogFooter as T, AlertDialogHeader as E, AlertDialogTitle as D, CTAOverflowMenu as O, IllustratedEmptyState as k, PagePurpose as ce } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
import { jsx as A, jsxs as j } from "react/jsx-runtime";
|
|
15
|
+
//#region src/ggnomad/pages/OperationsQueuePage.tsx
|
|
16
|
+
var M = {
|
|
17
|
+
GgnomadProperty: "PROPERTY",
|
|
18
|
+
GgnomadTour: "TOUR",
|
|
19
|
+
GgnomadEvent: "EVENT",
|
|
20
|
+
GgnomadActivity: "ACTIVITY"
|
|
21
|
+
};
|
|
22
|
+
function N() {
|
|
23
|
+
let { t: N } = se(), [P, F] = d(null), [I, L] = d(""), [R, z] = d(null), [B, V] = d(null), [H, U] = d(null), { data: W, refetch: G, loading: le } = y(te, { variables: {
|
|
24
|
+
limit: 20,
|
|
25
|
+
offset: 0
|
|
26
|
+
} }), K = u(() => W?.ggnomadListingReviewQueue ?? [], [W?.ggnomadListingReviewQueue]), { data: q, refetch: J, loading: ue } = y(ee, { variables: {
|
|
27
|
+
limit: 20,
|
|
28
|
+
offset: 0
|
|
29
|
+
} }), Y = u(() => q?.ggnomadBookingQueue ?? [], [q?.ggnomadBookingQueue]), [de, { loading: fe }] = v(e), [pe, { loading: X }] = v(i), [me, { loading: he }] = v(n), [Z, { loading: ge }] = v(r), [_e, { loading: Q }] = v(ne), [ve, { loading: $ }] = v(t), ye = async (e) => {
|
|
30
|
+
let t = M[e.__typename];
|
|
31
|
+
if (t) try {
|
|
32
|
+
let n = await de({ variables: {
|
|
33
|
+
listingType: t,
|
|
34
|
+
id: e.id
|
|
35
|
+
} });
|
|
36
|
+
if (n.error) {
|
|
37
|
+
l.error(N("ggnomad.opsQueue.approveErrorTitle", "Could not approve listing"), n.error.message);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
l.success(N("ggnomad.opsQueue.approveSuccessTitle", "Listing approved"), e.name), await G();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
l.error(N("ggnomad.opsQueue.approveErrorTitle", "Could not approve listing"), e instanceof Error ? e.message : String(e));
|
|
43
|
+
}
|
|
44
|
+
}, be = async () => {
|
|
45
|
+
if (P) {
|
|
46
|
+
if (!I.trim()) {
|
|
47
|
+
l.warning(N("ggnomad.opsQueue.reject.reasonRequiredTitle", "A reason is required"), N("ggnomad.opsQueue.reject.reasonRequiredBody", "Let the submitter know what needs fixing."));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
let e = await pe({ variables: {
|
|
52
|
+
listingType: P.listingType,
|
|
53
|
+
id: P.id,
|
|
54
|
+
reason: I.trim()
|
|
55
|
+
} });
|
|
56
|
+
if (e.error) {
|
|
57
|
+
l.error(N("ggnomad.opsQueue.rejectErrorTitle", "Could not reject listing"), e.error.message);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
l.success(N("ggnomad.opsQueue.rejectSuccessTitle", "Listing sent back to draft"), P.name), await G();
|
|
61
|
+
} catch (e) {
|
|
62
|
+
l.error(N("ggnomad.opsQueue.rejectErrorTitle", "Could not reject listing"), e instanceof Error ? e.message : String(e));
|
|
63
|
+
} finally {
|
|
64
|
+
F(null), L("");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, xe = async (e) => {
|
|
68
|
+
try {
|
|
69
|
+
let t = await me({ variables: { id: e.id } });
|
|
70
|
+
if (t.error) {
|
|
71
|
+
l.error(N("ggnomad.opsQueue.checkInErrorTitle", "Could not check in"), t.error.message);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
l.success(N("ggnomad.opsQueue.checkInSuccessTitle", "Checked in"), e.referenceCode), await J();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
l.error(N("ggnomad.opsQueue.checkInErrorTitle", "Could not check in"), e instanceof Error ? e.message : String(e));
|
|
77
|
+
}
|
|
78
|
+
}, Se = async (e) => {
|
|
79
|
+
try {
|
|
80
|
+
let t = await Z({ variables: { id: e.id } });
|
|
81
|
+
if (t.error) {
|
|
82
|
+
l.error(N("ggnomad.opsQueue.completeErrorTitle", "Could not complete booking"), t.error.message);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
l.success(N("ggnomad.opsQueue.completeSuccessTitle", "Booking completed"), e.referenceCode), await J();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
l.error(N("ggnomad.opsQueue.completeErrorTitle", "Could not complete booking"), e instanceof Error ? e.message : String(e));
|
|
88
|
+
}
|
|
89
|
+
}, Ce = async () => {
|
|
90
|
+
if (R) try {
|
|
91
|
+
let e = await _e({ variables: {
|
|
92
|
+
id: R.id,
|
|
93
|
+
reason: N("ggnomad.opsQueue.noShowReason", "Marked via operations queue")
|
|
94
|
+
} });
|
|
95
|
+
if (e.error) {
|
|
96
|
+
l.error(N("ggnomad.opsQueue.noShowErrorTitle", "Could not mark as no-show"), e.error.message);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
l.success(N("ggnomad.opsQueue.noShowSuccessTitle", "Marked as no-show"), R.referenceCode), await J();
|
|
100
|
+
} catch (e) {
|
|
101
|
+
l.error(N("ggnomad.opsQueue.noShowErrorTitle", "Could not mark as no-show"), e instanceof Error ? e.message : String(e));
|
|
102
|
+
} finally {
|
|
103
|
+
z(null);
|
|
104
|
+
}
|
|
105
|
+
}, we = async () => {
|
|
106
|
+
if (B) try {
|
|
107
|
+
let e = await ve({ variables: {
|
|
108
|
+
id: B.id,
|
|
109
|
+
reason: N("ggnomad.opsQueue.cancelReason", "Cancelled via operations queue")
|
|
110
|
+
} });
|
|
111
|
+
if (e.error) {
|
|
112
|
+
l.error(N("ggnomad.opsQueue.cancelErrorTitle", "Could not cancel booking"), e.error.message);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
l.success(N("ggnomad.opsQueue.cancelSuccessTitle", "Booking cancelled"), B.referenceCode), await J();
|
|
116
|
+
} catch (e) {
|
|
117
|
+
l.error(N("ggnomad.opsQueue.cancelErrorTitle", "Could not cancel booking"), e instanceof Error ? e.message : String(e));
|
|
118
|
+
} finally {
|
|
119
|
+
V(null);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return /* @__PURE__ */ j(a, {
|
|
123
|
+
title: N("ggnomad.opsQueue.title", "Operations Queue"),
|
|
124
|
+
titleAccent: N("ggnomad.opsQueue.title.accent", "Operations"),
|
|
125
|
+
subtitle: N("ggnomad.opsQueue.subtitle", "Verify listings before they go live and work the incoming booking queue"),
|
|
126
|
+
children: [
|
|
127
|
+
/* @__PURE__ */ A(ce, {
|
|
128
|
+
icon: /* @__PURE__ */ A(p, { className: "size-4" }),
|
|
129
|
+
children: N("ggnomad.opsQueue.purpose", "Listings move DRAFT → PENDING_REVIEW → ACTIVE — approve or reject submissions here before they can be booked. Bookings move PENDING → CONFIRMED → CHECKED_IN → COMPLETED — check guests in, complete stays, or mark a no-show once the scheduled date has arrived. Every action is recorded in the transition history.")
|
|
130
|
+
}),
|
|
131
|
+
/* @__PURE__ */ A(s, {
|
|
132
|
+
title: N("ggnomad.opsQueue.listingsTitle", "Listings awaiting review"),
|
|
133
|
+
children: !le && K.length === 0 ? /* @__PURE__ */ A(k, {
|
|
134
|
+
illustration: "empty-data",
|
|
135
|
+
title: N("ggnomad.opsQueue.listingsEmptyTitle", "Nothing waiting on you"),
|
|
136
|
+
description: N("ggnomad.opsQueue.listingsEmptyDescription", "Listings submitted for review will show up here in submission order.")
|
|
137
|
+
}) : /* @__PURE__ */ A("div", {
|
|
138
|
+
className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
|
|
139
|
+
children: K.map((e) => {
|
|
140
|
+
let t = M[e.__typename], n = e.pricePerNight ?? e.price ?? 0;
|
|
141
|
+
return /* @__PURE__ */ A(c, {
|
|
142
|
+
title: e.name,
|
|
143
|
+
meta: t,
|
|
144
|
+
price: `${e.currency} ${Number(n).toFixed(2)}`,
|
|
145
|
+
badge: /* @__PURE__ */ A(o, {
|
|
146
|
+
status: e.status,
|
|
147
|
+
live: !0
|
|
148
|
+
}),
|
|
149
|
+
footer: /* @__PURE__ */ A(O, {
|
|
150
|
+
align: "end",
|
|
151
|
+
actions: [
|
|
152
|
+
{
|
|
153
|
+
label: N("ggnomad.opsQueue.actions.approve", "Approve"),
|
|
154
|
+
onSelect: () => void ye(e),
|
|
155
|
+
disabled: fe,
|
|
156
|
+
icon: /* @__PURE__ */ A(f, {})
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
label: N("ggnomad.opsQueue.actions.reject", "Reject"),
|
|
160
|
+
onSelect: () => {
|
|
161
|
+
t && (F({
|
|
162
|
+
listingType: t,
|
|
163
|
+
id: e.id,
|
|
164
|
+
name: e.name
|
|
165
|
+
}), L(""));
|
|
166
|
+
},
|
|
167
|
+
intent: "destructive",
|
|
168
|
+
icon: /* @__PURE__ */ A(ae, {})
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
label: N("ggnomad.opsQueue.actions.history", "History"),
|
|
172
|
+
onSelect: () => U({
|
|
173
|
+
entityType: t ?? "PROPERTY",
|
|
174
|
+
entityId: e.id,
|
|
175
|
+
label: e.name
|
|
176
|
+
}),
|
|
177
|
+
icon: /* @__PURE__ */ A(m, {})
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
})
|
|
181
|
+
}, e.id);
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
}),
|
|
185
|
+
/* @__PURE__ */ A(s, {
|
|
186
|
+
title: N("ggnomad.opsQueue.bookingsTitle", "Booking queue"),
|
|
187
|
+
children: !ue && Y.length === 0 ? /* @__PURE__ */ A(k, {
|
|
188
|
+
illustration: "empty-data",
|
|
189
|
+
title: N("ggnomad.opsQueue.bookingsEmptyTitle", "No bookings need attention"),
|
|
190
|
+
description: N("ggnomad.opsQueue.bookingsEmptyDescription", "Pending, confirmed, and checked-in bookings appear here in scheduled-start order.")
|
|
191
|
+
}) : /* @__PURE__ */ A("div", {
|
|
192
|
+
className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-3",
|
|
193
|
+
children: Y.map((e) => {
|
|
194
|
+
let t = [];
|
|
195
|
+
return e.status === "CONFIRMED" && t.push({
|
|
196
|
+
label: N("ggnomad.opsQueue.actions.checkIn", "Check in"),
|
|
197
|
+
onSelect: () => void xe(e),
|
|
198
|
+
disabled: he,
|
|
199
|
+
icon: /* @__PURE__ */ A(h, {})
|
|
200
|
+
}, {
|
|
201
|
+
label: N("ggnomad.opsQueue.actions.noShow", "Mark no-show"),
|
|
202
|
+
onSelect: () => z({
|
|
203
|
+
id: e.id,
|
|
204
|
+
referenceCode: e.referenceCode
|
|
205
|
+
}),
|
|
206
|
+
disabled: Q,
|
|
207
|
+
icon: /* @__PURE__ */ A(_, {})
|
|
208
|
+
}), e.status === "CHECKED_IN" && t.push({
|
|
209
|
+
label: N("ggnomad.opsQueue.actions.complete", "Complete"),
|
|
210
|
+
onSelect: () => void Se(e),
|
|
211
|
+
disabled: ge,
|
|
212
|
+
icon: /* @__PURE__ */ A(g, {})
|
|
213
|
+
}), t.push({
|
|
214
|
+
label: N("ggnomad.opsQueue.actions.cancel", "Cancel"),
|
|
215
|
+
onSelect: () => V({
|
|
216
|
+
id: e.id,
|
|
217
|
+
referenceCode: e.referenceCode
|
|
218
|
+
}),
|
|
219
|
+
disabled: $,
|
|
220
|
+
icon: /* @__PURE__ */ A(oe, {}),
|
|
221
|
+
intent: "destructive"
|
|
222
|
+
}, {
|
|
223
|
+
label: N("ggnomad.opsQueue.actions.history", "History"),
|
|
224
|
+
onSelect: () => U({
|
|
225
|
+
entityType: "BOOKING",
|
|
226
|
+
entityId: e.id,
|
|
227
|
+
label: e.referenceCode
|
|
228
|
+
}),
|
|
229
|
+
icon: /* @__PURE__ */ A(m, {})
|
|
230
|
+
}), /* @__PURE__ */ A(c, {
|
|
231
|
+
title: e.referenceCode,
|
|
232
|
+
meta: `${e.itemName} · ${e.guestName}`,
|
|
233
|
+
price: `${e.currency} ${Number(e.totalPrice).toFixed(2)}`,
|
|
234
|
+
badge: /* @__PURE__ */ A(o, {
|
|
235
|
+
status: e.status,
|
|
236
|
+
live: !0
|
|
237
|
+
}),
|
|
238
|
+
footer: /* @__PURE__ */ A(O, {
|
|
239
|
+
align: "end",
|
|
240
|
+
actions: t
|
|
241
|
+
})
|
|
242
|
+
}, e.id);
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
}),
|
|
246
|
+
/* @__PURE__ */ A(b, {
|
|
247
|
+
open: P !== null,
|
|
248
|
+
onOpenChange: (e) => {
|
|
249
|
+
e || (F(null), L(""));
|
|
250
|
+
},
|
|
251
|
+
children: /* @__PURE__ */ j(C, { children: [
|
|
252
|
+
/* @__PURE__ */ j(E, { children: [/* @__PURE__ */ A(D, { children: N("ggnomad.opsQueue.rejectDialog.title", "Reject this listing?") }), /* @__PURE__ */ A(w, { children: N("ggnomad.opsQueue.rejectDialog.body", "This sends {{name}} back to DRAFT. The reason is recorded.").replace("{{name}}", P?.name ?? "") })] }),
|
|
253
|
+
/* @__PURE__ */ j("div", { children: [/* @__PURE__ */ A("label", {
|
|
254
|
+
htmlFor: "ops-reject-reason",
|
|
255
|
+
className: "block text-sm font-medium text-text-primary",
|
|
256
|
+
children: N("ggnomad.opsQueue.rejectDialog.reasonLabel", "Reason")
|
|
257
|
+
}), /* @__PURE__ */ A("textarea", {
|
|
258
|
+
id: "ops-reject-reason",
|
|
259
|
+
value: I,
|
|
260
|
+
onChange: (e) => L(e.target.value),
|
|
261
|
+
disabled: X,
|
|
262
|
+
rows: 3,
|
|
263
|
+
className: "mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50",
|
|
264
|
+
placeholder: N("ggnomad.opsQueue.rejectDialog.reasonPlaceholder", "e.g. Missing required photos")
|
|
265
|
+
})] }),
|
|
266
|
+
/* @__PURE__ */ j(T, { children: [/* @__PURE__ */ A(S, {
|
|
267
|
+
disabled: X,
|
|
268
|
+
children: N("ggnomad.opsQueue.rejectDialog.cancel", "Keep pending")
|
|
269
|
+
}), /* @__PURE__ */ A(x, {
|
|
270
|
+
className: "bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover",
|
|
271
|
+
disabled: X,
|
|
272
|
+
onClick: (e) => {
|
|
273
|
+
e.preventDefault(), be();
|
|
274
|
+
},
|
|
275
|
+
children: X ? N("ggnomad.opsQueue.rejectDialog.rejecting", "Rejecting…") : N("ggnomad.opsQueue.rejectDialog.confirm", "Reject listing")
|
|
276
|
+
})] })
|
|
277
|
+
] })
|
|
278
|
+
}),
|
|
279
|
+
/* @__PURE__ */ A(b, {
|
|
280
|
+
open: R !== null,
|
|
281
|
+
onOpenChange: (e) => !e && z(null),
|
|
282
|
+
children: /* @__PURE__ */ j(C, { children: [/* @__PURE__ */ j(E, { children: [/* @__PURE__ */ A(D, { children: N("ggnomad.opsQueue.noShowDialog.title", "Mark as no-show?") }), /* @__PURE__ */ A(w, { children: N("ggnomad.opsQueue.noShowDialog.body", "{{ref}} will be marked NO_SHOW. Only allowed once the scheduled start has arrived.").replace("{{ref}}", R?.referenceCode ?? "") })] }), /* @__PURE__ */ j(T, { children: [/* @__PURE__ */ A(S, {
|
|
283
|
+
disabled: Q,
|
|
284
|
+
children: N("ggnomad.opsQueue.noShowDialog.cancel", "Keep booking")
|
|
285
|
+
}), /* @__PURE__ */ j(x, {
|
|
286
|
+
disabled: Q,
|
|
287
|
+
onClick: (e) => {
|
|
288
|
+
e.preventDefault(), Ce();
|
|
289
|
+
},
|
|
290
|
+
children: [/* @__PURE__ */ A(ie, { className: "mr-1 size-4" }), Q ? N("ggnomad.opsQueue.noShowDialog.marking", "Marking…") : N("ggnomad.opsQueue.noShowDialog.confirm", "Mark no-show")]
|
|
291
|
+
})] })] })
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ A(b, {
|
|
294
|
+
open: B !== null,
|
|
295
|
+
onOpenChange: (e) => !e && V(null),
|
|
296
|
+
children: /* @__PURE__ */ j(C, { children: [/* @__PURE__ */ j(E, { children: [/* @__PURE__ */ A(D, { children: N("ggnomad.opsQueue.cancelDialog.title", "Cancel this booking?") }), /* @__PURE__ */ A(w, { children: N("ggnomad.opsQueue.cancelDialog.body", "This releases reservation {{ref}} and cannot be undone.").replace("{{ref}}", B?.referenceCode ?? "") })] }), /* @__PURE__ */ j(T, { children: [/* @__PURE__ */ A(S, {
|
|
297
|
+
disabled: $,
|
|
298
|
+
children: N("ggnomad.opsQueue.cancelDialog.keep", "Keep booking")
|
|
299
|
+
}), /* @__PURE__ */ A(x, {
|
|
300
|
+
className: "bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover",
|
|
301
|
+
disabled: $,
|
|
302
|
+
onClick: (e) => {
|
|
303
|
+
e.preventDefault(), we();
|
|
304
|
+
},
|
|
305
|
+
children: $ ? N("ggnomad.opsQueue.cancelDialog.cancelling", "Cancelling…") : N("ggnomad.opsQueue.cancelDialog.confirm", "Cancel booking")
|
|
306
|
+
})] })] })
|
|
307
|
+
}),
|
|
308
|
+
H ? /* @__PURE__ */ A(re, {
|
|
309
|
+
open: H !== null,
|
|
310
|
+
onOpenChange: (e) => {
|
|
311
|
+
e || U(null);
|
|
312
|
+
},
|
|
313
|
+
entityType: H.entityType,
|
|
314
|
+
entityId: H.entityId,
|
|
315
|
+
label: H.label
|
|
316
|
+
}) : null
|
|
317
|
+
]
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
export { N as OperationsQueuePage };
|
|
322
|
+
|
|
323
|
+
//# sourceMappingURL=OperationsQueuePage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OperationsQueuePage.js","names":[],"sources":["../../../src/ggnomad/pages/OperationsQueuePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport type { TypedDocumentNode } from '@apollo/client';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Check,\n ClipboardList,\n History,\n LogIn,\n LogOut,\n ShieldQuestion,\n UserX,\n X,\n XCircle,\n} from 'lucide-react';\nimport type { CTAAction } from '@burdenoff/fe-libs/ui';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n CTAOverflowMenu,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n APPROVE_GGNOMAD_LISTING_MUTATION,\n CANCEL_GGNOMAD_BOOKING_MUTATION,\n CHECK_IN_GGNOMAD_BOOKING_MUTATION,\n COMPLETE_GGNOMAD_BOOKING_MUTATION,\n GGNOMAD_BOOKING_QUEUE_QUERY,\n GGNOMAD_LISTING_REVIEW_QUEUE_QUERY,\n MARK_GGNOMAD_BOOKING_NO_SHOW_MUTATION,\n REJECT_GGNOMAD_LISTING_MUTATION,\n} from '../graphql';\nimport { PageShell } from './PageShell';\nimport { AuditTrailDialog, EntityCard, SectionCard, StatusBadge } from '../components';\nimport { notify } from '../utils/notify';\n\ntype ListingType = 'PROPERTY' | 'TOUR' | 'EVENT' | 'ACTIVITY';\n\ninterface QueuedListing {\n __typename: string;\n id: string;\n name: string;\n status: string;\n price?: number;\n pricePerNight?: number;\n currency: string;\n providerId: string | null;\n}\n\ninterface QueuedBooking {\n id: string;\n referenceCode: string;\n bookingType: string;\n status: string;\n itemName: string;\n guestName: string;\n guests: number;\n checkIn: string | null;\n checkOut: string | null;\n eventDate: string | null;\n totalPrice: number;\n currency: string;\n}\n\nconst LISTING_TYPE_BY_TYPENAME: Record<string, ListingType> = {\n GgnomadProperty: 'PROPERTY',\n GgnomadTour: 'TOUR',\n GgnomadEvent: 'EVENT',\n GgnomadActivity: 'ACTIVITY',\n};\n\ntype HistoryTarget = { entityType: string; entityId: string; label: string };\ntype RejectTarget = { listingType: ListingType; id: string; name: string };\ntype NoShowTarget = { id: string; referenceCode: string };\ntype CancelTarget = { id: string; referenceCode: string };\n\n/**\n * BOFF-4388 — the operational side of the marketplace: a marketplace operator\n * (or host-operator for their own submissions) reviews PENDING_REVIEW\n * listings and works the incoming booking queue from one place. Every action\n * here calls a real, guarded mutation on wspace-ggnomad-svc — approve/reject\n * on listings are gated server-side by the `ggnomad:manage` RBAC action\n * (marketplace operator only); check-in/complete/no-show/cancel are gated by\n * `ggnomad:update` (host-operator and above).\n */\nexport function OperationsQueuePage() {\n const { t } = useI18n();\n const [rejectTarget, setRejectTarget] = useState<RejectTarget | null>(null);\n const [rejectReason, setRejectReason] = useState('');\n const [noShowTarget, setNoShowTarget] = useState<NoShowTarget | null>(null);\n const [cancelTarget, setCancelTarget] = useState<CancelTarget | null>(null);\n const [historyTarget, setHistoryTarget] = useState<HistoryTarget | null>(null);\n\n const {\n data: listingData,\n refetch: refetchListings,\n loading: listingsLoading,\n } = useQuery(\n GGNOMAD_LISTING_REVIEW_QUEUE_QUERY as TypedDocumentNode<\n { ggnomadListingReviewQueue: QueuedListing[] },\n { limit?: number; offset?: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n const listings = useMemo(\n () => listingData?.ggnomadListingReviewQueue ?? [],\n [listingData?.ggnomadListingReviewQueue]\n );\n\n const {\n data: bookingData,\n refetch: refetchBookings,\n loading: bookingsLoading,\n } = useQuery(\n GGNOMAD_BOOKING_QUEUE_QUERY as TypedDocumentNode<\n { ggnomadBookingQueue: QueuedBooking[] },\n { limit?: number; offset?: number }\n >,\n { variables: { limit: 20, offset: 0 } }\n );\n const bookings = useMemo(\n () => bookingData?.ggnomadBookingQueue ?? [],\n [bookingData?.ggnomadBookingQueue]\n );\n\n const [approveListing, { loading: approving }] = useMutation(\n APPROVE_GGNOMAD_LISTING_MUTATION as TypedDocumentNode<\n { approveGgnomadListing: { id: string; status: string } },\n { listingType: ListingType; id: string; note?: string }\n >\n );\n const [rejectListing, { loading: rejecting }] = useMutation(\n REJECT_GGNOMAD_LISTING_MUTATION as TypedDocumentNode<\n { rejectGgnomadListing: { id: string; status: string } },\n { listingType: ListingType; id: string; reason: string }\n >\n );\n const [checkInBooking, { loading: checkingIn }] = useMutation(\n CHECK_IN_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { checkInGgnomadBooking: { id: string } },\n { id: string }\n >\n );\n const [completeBooking, { loading: completing }] = useMutation(\n COMPLETE_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { completeGgnomadBooking: { id: string } },\n { id: string }\n >\n );\n const [markNoShow, { loading: markingNoShow }] = useMutation(\n MARK_GGNOMAD_BOOKING_NO_SHOW_MUTATION as TypedDocumentNode<\n { markGgnomadBookingNoShow: { id: string } },\n { id: string; reason?: string }\n >\n );\n const [cancelBooking, { loading: cancelling }] = useMutation(\n CANCEL_GGNOMAD_BOOKING_MUTATION as TypedDocumentNode<\n { cancelGgnomadBooking: { id: string } },\n { id: string; reason: string }\n >\n );\n\n const handleApprove = async (listing: QueuedListing) => {\n const listingType = LISTING_TYPE_BY_TYPENAME[listing.__typename];\n if (!listingType) return;\n try {\n const result = await approveListing({ variables: { listingType, id: listing.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.approveErrorTitle', 'Could not approve listing'),\n result.error.message\n );\n return;\n }\n notify.success(t('ggnomad.opsQueue.approveSuccessTitle', 'Listing approved'), listing.name);\n await refetchListings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.approveErrorTitle', 'Could not approve listing'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmReject = async () => {\n if (!rejectTarget) return;\n if (!rejectReason.trim()) {\n notify.warning(\n t('ggnomad.opsQueue.reject.reasonRequiredTitle', 'A reason is required'),\n t('ggnomad.opsQueue.reject.reasonRequiredBody', 'Let the submitter know what needs fixing.')\n );\n return;\n }\n try {\n const result = await rejectListing({\n variables: {\n listingType: rejectTarget.listingType,\n id: rejectTarget.id,\n reason: rejectReason.trim(),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.rejectErrorTitle', 'Could not reject listing'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.rejectSuccessTitle', 'Listing sent back to draft'),\n rejectTarget.name\n );\n await refetchListings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.rejectErrorTitle', 'Could not reject listing'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setRejectTarget(null);\n setRejectReason('');\n }\n };\n\n const handleCheckIn = async (booking: QueuedBooking) => {\n try {\n const result = await checkInBooking({ variables: { id: booking.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.checkInErrorTitle', 'Could not check in'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.checkInSuccessTitle', 'Checked in'),\n booking.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.checkInErrorTitle', 'Could not check in'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const handleComplete = async (booking: QueuedBooking) => {\n try {\n const result = await completeBooking({ variables: { id: booking.id } });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.completeErrorTitle', 'Could not complete booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.completeSuccessTitle', 'Booking completed'),\n booking.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.completeErrorTitle', 'Could not complete booking'),\n err instanceof Error ? err.message : String(err)\n );\n }\n };\n\n const confirmNoShow = async () => {\n if (!noShowTarget) return;\n try {\n const result = await markNoShow({\n variables: {\n id: noShowTarget.id,\n reason: t('ggnomad.opsQueue.noShowReason', 'Marked via operations queue'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.noShowErrorTitle', 'Could not mark as no-show'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.noShowSuccessTitle', 'Marked as no-show'),\n noShowTarget.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.noShowErrorTitle', 'Could not mark as no-show'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setNoShowTarget(null);\n }\n };\n\n const confirmCancel = async () => {\n if (!cancelTarget) return;\n try {\n const result = await cancelBooking({\n variables: {\n id: cancelTarget.id,\n reason: t('ggnomad.opsQueue.cancelReason', 'Cancelled via operations queue'),\n },\n });\n if (result.error) {\n notify.error(\n t('ggnomad.opsQueue.cancelErrorTitle', 'Could not cancel booking'),\n result.error.message\n );\n return;\n }\n notify.success(\n t('ggnomad.opsQueue.cancelSuccessTitle', 'Booking cancelled'),\n cancelTarget.referenceCode\n );\n await refetchBookings();\n } catch (err) {\n notify.error(\n t('ggnomad.opsQueue.cancelErrorTitle', 'Could not cancel booking'),\n err instanceof Error ? err.message : String(err)\n );\n } finally {\n setCancelTarget(null);\n }\n };\n\n return (\n <PageShell\n title={t('ggnomad.opsQueue.title', 'Operations Queue')}\n titleAccent={t('ggnomad.opsQueue.title.accent', 'Operations')}\n subtitle={t(\n 'ggnomad.opsQueue.subtitle',\n 'Verify listings before they go live and work the incoming booking queue'\n )}\n >\n <PagePurpose icon={<ClipboardList className=\"size-4\" />}>\n {t(\n 'ggnomad.opsQueue.purpose',\n 'Listings move DRAFT → PENDING_REVIEW → ACTIVE — approve or reject submissions here before they can be booked. Bookings move PENDING → CONFIRMED → CHECKED_IN → COMPLETED — check guests in, complete stays, or mark a no-show once the scheduled date has arrived. Every action is recorded in the transition history.'\n )}\n </PagePurpose>\n\n <SectionCard title={t('ggnomad.opsQueue.listingsTitle', 'Listings awaiting review')}>\n {!listingsLoading && listings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.opsQueue.listingsEmptyTitle', 'Nothing waiting on you')}\n description={t(\n 'ggnomad.opsQueue.listingsEmptyDescription',\n 'Listings submitted for review will show up here in submission order.'\n )}\n />\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {listings.map((listing) => {\n const listingType = LISTING_TYPE_BY_TYPENAME[listing.__typename];\n const price = listing.pricePerNight ?? listing.price ?? 0;\n return (\n <EntityCard\n key={listing.id}\n title={listing.name}\n meta={listingType}\n price={`${listing.currency} ${Number(price).toFixed(2)}`}\n badge={<StatusBadge status={listing.status} live />}\n footer={\n <CTAOverflowMenu\n align=\"end\"\n actions={[\n {\n label: t('ggnomad.opsQueue.actions.approve', 'Approve'),\n onSelect: () => void handleApprove(listing),\n disabled: approving,\n icon: <Check />,\n },\n {\n label: t('ggnomad.opsQueue.actions.reject', 'Reject'),\n onSelect: () => {\n if (!listingType) return;\n setRejectTarget({ listingType, id: listing.id, name: listing.name });\n setRejectReason('');\n },\n intent: 'destructive',\n icon: <X />,\n },\n {\n label: t('ggnomad.opsQueue.actions.history', 'History'),\n onSelect: () =>\n setHistoryTarget({\n entityType: listingType ?? 'PROPERTY',\n entityId: listing.id,\n label: listing.name,\n }),\n icon: <History />,\n },\n ]}\n />\n }\n />\n );\n })}\n </div>\n )}\n </SectionCard>\n\n <SectionCard title={t('ggnomad.opsQueue.bookingsTitle', 'Booking queue')}>\n {!bookingsLoading && bookings.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('ggnomad.opsQueue.bookingsEmptyTitle', 'No bookings need attention')}\n description={t(\n 'ggnomad.opsQueue.bookingsEmptyDescription',\n 'Pending, confirmed, and checked-in bookings appear here in scheduled-start order.'\n )}\n />\n ) : (\n <div className=\"grid gap-3 sm:grid-cols-2 xl:grid-cols-3\">\n {bookings.map((booking) => {\n const actions: CTAAction[] = [];\n if (booking.status === 'CONFIRMED') {\n actions.push(\n {\n label: t('ggnomad.opsQueue.actions.checkIn', 'Check in'),\n onSelect: () => void handleCheckIn(booking),\n disabled: checkingIn,\n icon: <LogIn />,\n },\n {\n label: t('ggnomad.opsQueue.actions.noShow', 'Mark no-show'),\n onSelect: () =>\n setNoShowTarget({ id: booking.id, referenceCode: booking.referenceCode }),\n disabled: markingNoShow,\n icon: <ShieldQuestion />,\n }\n );\n }\n if (booking.status === 'CHECKED_IN') {\n actions.push({\n label: t('ggnomad.opsQueue.actions.complete', 'Complete'),\n onSelect: () => void handleComplete(booking),\n disabled: completing,\n icon: <LogOut />,\n });\n }\n actions.push(\n {\n label: t('ggnomad.opsQueue.actions.cancel', 'Cancel'),\n onSelect: () =>\n setCancelTarget({ id: booking.id, referenceCode: booking.referenceCode }),\n disabled: cancelling,\n icon: <XCircle />,\n intent: 'destructive',\n },\n {\n label: t('ggnomad.opsQueue.actions.history', 'History'),\n onSelect: () =>\n setHistoryTarget({\n entityType: 'BOOKING',\n entityId: booking.id,\n label: booking.referenceCode,\n }),\n icon: <History />,\n }\n );\n\n return (\n <EntityCard\n key={booking.id}\n title={booking.referenceCode}\n meta={`${booking.itemName} · ${booking.guestName}`}\n price={`${booking.currency} ${Number(booking.totalPrice).toFixed(2)}`}\n badge={<StatusBadge status={booking.status} live />}\n footer={<CTAOverflowMenu align=\"end\" actions={actions} />}\n />\n );\n })}\n </div>\n )}\n </SectionCard>\n\n <AlertDialog\n open={rejectTarget !== null}\n onOpenChange={(open) => {\n if (!open) {\n setRejectTarget(null);\n setRejectReason('');\n }\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.rejectDialog.title', 'Reject this listing?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.rejectDialog.body',\n 'This sends {{name}} back to DRAFT. The reason is recorded.'\n ).replace('{{name}}', rejectTarget?.name ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <div>\n <label\n htmlFor=\"ops-reject-reason\"\n className=\"block text-sm font-medium text-text-primary\"\n >\n {t('ggnomad.opsQueue.rejectDialog.reasonLabel', 'Reason')}\n </label>\n <textarea\n id=\"ops-reject-reason\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n disabled={rejecting}\n rows={3}\n className=\"mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50\"\n placeholder={t(\n 'ggnomad.opsQueue.rejectDialog.reasonPlaceholder',\n 'e.g. Missing required photos'\n )}\n />\n </div>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={rejecting}>\n {t('ggnomad.opsQueue.rejectDialog.cancel', 'Keep pending')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={rejecting}\n onClick={(event) => {\n event.preventDefault();\n void confirmReject();\n }}\n >\n {rejecting\n ? t('ggnomad.opsQueue.rejectDialog.rejecting', 'Rejecting…')\n : t('ggnomad.opsQueue.rejectDialog.confirm', 'Reject listing')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <AlertDialog\n open={noShowTarget !== null}\n onOpenChange={(open) => !open && setNoShowTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.noShowDialog.title', 'Mark as no-show?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.noShowDialog.body',\n '{{ref}} will be marked NO_SHOW. Only allowed once the scheduled start has arrived.'\n ).replace('{{ref}}', noShowTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={markingNoShow}>\n {t('ggnomad.opsQueue.noShowDialog.cancel', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n disabled={markingNoShow}\n onClick={(event) => {\n event.preventDefault();\n void confirmNoShow();\n }}\n >\n <UserX className=\"mr-1 size-4\" />\n {markingNoShow\n ? t('ggnomad.opsQueue.noShowDialog.marking', 'Marking…')\n : t('ggnomad.opsQueue.noShowDialog.confirm', 'Mark no-show')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <AlertDialog\n open={cancelTarget !== null}\n onOpenChange={(open) => !open && setCancelTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>\n {t('ggnomad.opsQueue.cancelDialog.title', 'Cancel this booking?')}\n </AlertDialogTitle>\n <AlertDialogDescription>\n {t(\n 'ggnomad.opsQueue.cancelDialog.body',\n 'This releases reservation {{ref}} and cannot be undone.'\n ).replace('{{ref}}', cancelTarget?.referenceCode ?? '')}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={cancelling}>\n {t('ggnomad.opsQueue.cancelDialog.keep', 'Keep booking')}\n </AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover\"\n disabled={cancelling}\n onClick={(event) => {\n event.preventDefault();\n void confirmCancel();\n }}\n >\n {cancelling\n ? t('ggnomad.opsQueue.cancelDialog.cancelling', 'Cancelling…')\n : t('ggnomad.opsQueue.cancelDialog.confirm', 'Cancel booking')}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n {historyTarget ? (\n <AuditTrailDialog\n open={historyTarget !== null}\n onOpenChange={(open) => {\n if (!open) setHistoryTarget(null);\n }}\n entityType={historyTarget.entityType}\n entityId={historyTarget.entityId}\n label={historyTarget.label}\n />\n ) : null}\n </PageShell>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manager dashboard for corporate travel governance (BOFF-4365): consolidated
|
|
3
|
+
* spend, the pending-approval queue for bookings that violated policy, and
|
|
4
|
+
* the travel policies themselves (budget caps, blocked destinations, class
|
|
5
|
+
* rules). Approve/reject and policy CRUD are gated server-side by the
|
|
6
|
+
* `ggnomad:manage` RBAC action — a traveler or host-operator cannot act here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function TravelPolicyPage(): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=TravelPolicyPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TravelPolicyPage.d.ts","sourceRoot":"","sources":["../../../src/ggnomad/pages/TravelPolicyPage.tsx"],"names":[],"mappings":"AA+EA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,gCA+Z/B"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { APPROVE_GGNOMAD_BOOKING_APPROVAL_MUTATION as e, DELETE_GGNOMAD_TRAVEL_POLICY_MUTATION as t, GGNOMAD_BOOKING_APPROVALS_QUERY as ee, GGNOMAD_SPEND_SUMMARY_QUERY as n, GGNOMAD_TRAVEL_POLICIES_QUERY as r, REJECT_GGNOMAD_BOOKING_APPROVAL_MUTATION as i } from "../graphql.js";
|
|
2
|
+
import { PageShell as te } from "./PageShell.js";
|
|
3
|
+
import { StatCard as a } from "../components/StatCard.js";
|
|
4
|
+
import { StatusBadge as o } from "../components/StatusBadge.js";
|
|
5
|
+
import { SectionCard as s } from "../components/SectionCard.js";
|
|
6
|
+
import { EntityCard as c } from "../components/EntityCard.js";
|
|
7
|
+
import { notify as l } from "../utils/notify.js";
|
|
8
|
+
import { CreateTravelPolicyDialog as ne } from "../components/CreateTravelPolicyDialog.js";
|
|
9
|
+
import "../components/index.js";
|
|
10
|
+
import { useMemo as u, useState as d } from "react";
|
|
11
|
+
import { AlertCircle as f, Check as re, DollarSign as p, Plus as m, ShieldCheck as h, Trash2 as g, Wallet as ie, X as ae } from "lucide-react";
|
|
12
|
+
import { useI18n as _ } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
13
|
+
import { useMutation as v, useQuery as y } from "@apollo/client/react";
|
|
14
|
+
import { AlertDialog as b, AlertDialogAction as x, AlertDialogCancel as S, AlertDialogContent as C, AlertDialogDescription as w, AlertDialogFooter as T, AlertDialogHeader as E, AlertDialogTitle as D, CTAOverflowMenu as O, IllustratedEmptyState as k, PagePurpose as A } from "@burdenoff/fe-libs/ui";
|
|
15
|
+
import { jsx as j, jsxs as M } from "react/jsx-runtime";
|
|
16
|
+
//#region src/ggnomad/pages/TravelPolicyPage.tsx
|
|
17
|
+
function N() {
|
|
18
|
+
let { t: N } = _(), [oe, P] = d(!1), [F, I] = d(null), [L, R] = d(""), [z, B] = d(null), { data: V, refetch: H } = y(n), U = V?.ggnomadSpendSummary, { data: W, refetch: G, loading: K } = y(ee, { variables: {
|
|
19
|
+
status: "PENDING",
|
|
20
|
+
limit: 20,
|
|
21
|
+
offset: 0
|
|
22
|
+
} }), q = u(() => W?.ggnomadBookingApprovals ?? [], [W?.ggnomadBookingApprovals]), { data: J, refetch: Y } = y(r, { variables: {
|
|
23
|
+
limit: 50,
|
|
24
|
+
offset: 0
|
|
25
|
+
} }), X = u(() => J?.ggnomadTravelPolicies ?? [], [J?.ggnomadTravelPolicies]), [se, { loading: ce }] = v(e), [le, { loading: Z }] = v(i), [ue, { loading: Q }] = v(t), $ = async () => {
|
|
26
|
+
await Promise.all([
|
|
27
|
+
H(),
|
|
28
|
+
G(),
|
|
29
|
+
Y()
|
|
30
|
+
]);
|
|
31
|
+
}, de = async (e) => {
|
|
32
|
+
try {
|
|
33
|
+
let t = await se({ variables: { id: e.id } });
|
|
34
|
+
if (t.error) {
|
|
35
|
+
l.error(N("ggnomad.travelPolicy.approve.errorTitle", "Could not approve booking"), t.error.message);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
l.success(N("ggnomad.travelPolicy.approve.successTitle", "Booking approved"), e.booking?.referenceCode ?? e.bookingId), await $();
|
|
39
|
+
} catch (e) {
|
|
40
|
+
l.error(N("ggnomad.travelPolicy.approve.errorTitle", "Could not approve booking"), e instanceof Error ? e.message : String(e));
|
|
41
|
+
}
|
|
42
|
+
}, fe = async () => {
|
|
43
|
+
if (F) {
|
|
44
|
+
if (!L.trim()) {
|
|
45
|
+
l.warning(N("ggnomad.travelPolicy.reject.reasonRequiredTitle", "A reason is required"), N("ggnomad.travelPolicy.reject.reasonRequiredBody", "Let the traveler know why this was rejected."));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
let e = await le({ variables: {
|
|
50
|
+
id: F.id,
|
|
51
|
+
reason: L.trim()
|
|
52
|
+
} });
|
|
53
|
+
if (e.error) {
|
|
54
|
+
l.error(N("ggnomad.travelPolicy.reject.errorTitle", "Could not reject booking"), e.error.message);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
l.success(N("ggnomad.travelPolicy.reject.successTitle", "Booking rejected"), F.booking?.referenceCode ?? F.bookingId), await $();
|
|
58
|
+
} catch (e) {
|
|
59
|
+
l.error(N("ggnomad.travelPolicy.reject.errorTitle", "Could not reject booking"), e instanceof Error ? e.message : String(e));
|
|
60
|
+
} finally {
|
|
61
|
+
I(null), R("");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, pe = async () => {
|
|
65
|
+
if (z) try {
|
|
66
|
+
let e = await ue({ variables: { id: z.id } });
|
|
67
|
+
if (e.error) {
|
|
68
|
+
l.error(N("ggnomad.travelPolicy.delete.errorTitle", "Could not remove policy"), e.error.message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
l.success(N("ggnomad.travelPolicy.delete.successTitle", "Policy removed"), z.name), await Y();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
l.error(N("ggnomad.travelPolicy.delete.errorTitle", "Could not remove policy"), e instanceof Error ? e.message : String(e));
|
|
74
|
+
} finally {
|
|
75
|
+
B(null);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return /* @__PURE__ */ M(te, {
|
|
79
|
+
title: N("ggnomad.travelPolicy.title", "Travel Policy"),
|
|
80
|
+
titleAccent: N("ggnomad.travelPolicy.title.accent", "Policy"),
|
|
81
|
+
subtitle: N("ggnomad.travelPolicy.subtitle", "Corporate travel guardrails: budgets, blocked destinations, class rules, and booking approvals"),
|
|
82
|
+
actions: /* @__PURE__ */ j(O, {
|
|
83
|
+
primary: {
|
|
84
|
+
label: N("ggnomad.travelPolicy.actions.newPolicy", "New policy"),
|
|
85
|
+
onSelect: () => P(!0),
|
|
86
|
+
icon: /* @__PURE__ */ j(m, {})
|
|
87
|
+
},
|
|
88
|
+
actions: []
|
|
89
|
+
}),
|
|
90
|
+
children: [
|
|
91
|
+
/* @__PURE__ */ j(A, {
|
|
92
|
+
icon: /* @__PURE__ */ j(h, { className: "size-4" }),
|
|
93
|
+
children: N("ggnomad.travelPolicy.purpose", "Managers define budget caps, blocked destinations and class rules here. A booking that violates the applicable policy is held for your approval instead of confirming automatically — travelers and hosts cannot approve their own over-budget bookings.")
|
|
94
|
+
}),
|
|
95
|
+
/* @__PURE__ */ M("section", {
|
|
96
|
+
className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4",
|
|
97
|
+
children: [
|
|
98
|
+
/* @__PURE__ */ j(a, {
|
|
99
|
+
hero: !0,
|
|
100
|
+
label: N("ggnomad.travelPolicy.stats.totalSpend", "Total spend"),
|
|
101
|
+
value: `$${Number(U?.totalSpend ?? 0).toLocaleString()}`,
|
|
102
|
+
icon: /* @__PURE__ */ j(p, {})
|
|
103
|
+
}),
|
|
104
|
+
/* @__PURE__ */ j(a, {
|
|
105
|
+
label: N("ggnomad.travelPolicy.stats.bookingCount", "Bookings"),
|
|
106
|
+
value: Number(U?.bookingCount ?? 0).toLocaleString(),
|
|
107
|
+
icon: /* @__PURE__ */ j(ie, {})
|
|
108
|
+
}),
|
|
109
|
+
/* @__PURE__ */ j(a, {
|
|
110
|
+
label: N("ggnomad.travelPolicy.stats.pendingCount", "Pending approvals"),
|
|
111
|
+
value: Number(U?.pendingApprovalCount ?? 0).toLocaleString(),
|
|
112
|
+
icon: /* @__PURE__ */ j(f, {})
|
|
113
|
+
}),
|
|
114
|
+
/* @__PURE__ */ j(a, {
|
|
115
|
+
label: N("ggnomad.travelPolicy.stats.pendingAmount", "Pending amount"),
|
|
116
|
+
value: `$${Number(U?.pendingApprovalAmount ?? 0).toLocaleString()}`,
|
|
117
|
+
icon: /* @__PURE__ */ j(h, {})
|
|
118
|
+
})
|
|
119
|
+
]
|
|
120
|
+
}),
|
|
121
|
+
/* @__PURE__ */ j(s, {
|
|
122
|
+
title: N("ggnomad.travelPolicy.approvalsTitle", "Pending approvals"),
|
|
123
|
+
children: !K && q.length === 0 ? /* @__PURE__ */ j(k, {
|
|
124
|
+
illustration: "empty-data",
|
|
125
|
+
title: N("ggnomad.travelPolicy.approvalsEmptyTitle", "Nothing waiting on you"),
|
|
126
|
+
description: N("ggnomad.travelPolicy.approvalsEmptyDescription", "Bookings that exceed a travel policy will show up here for your decision.")
|
|
127
|
+
}) : /* @__PURE__ */ j("div", {
|
|
128
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
129
|
+
children: q.map((e) => /* @__PURE__ */ j(c, {
|
|
130
|
+
title: e.booking?.referenceCode ?? e.bookingId,
|
|
131
|
+
meta: e.reasons.join(" · ") || e.booking?.itemName,
|
|
132
|
+
price: e.booking ? `${e.booking.currency} ${Number(e.booking.totalPrice).toFixed(2)}` : void 0,
|
|
133
|
+
badge: /* @__PURE__ */ j(o, {
|
|
134
|
+
status: e.status,
|
|
135
|
+
live: !0
|
|
136
|
+
}),
|
|
137
|
+
footer: /* @__PURE__ */ j(O, {
|
|
138
|
+
align: "end",
|
|
139
|
+
actions: [{
|
|
140
|
+
label: N("ggnomad.travelPolicy.actions.approve", "Approve"),
|
|
141
|
+
onSelect: () => void de(e),
|
|
142
|
+
disabled: ce,
|
|
143
|
+
icon: /* @__PURE__ */ j(re, {})
|
|
144
|
+
}, {
|
|
145
|
+
label: N("ggnomad.travelPolicy.actions.reject", "Reject"),
|
|
146
|
+
onSelect: () => {
|
|
147
|
+
I(e), R("");
|
|
148
|
+
},
|
|
149
|
+
intent: "destructive",
|
|
150
|
+
icon: /* @__PURE__ */ j(ae, {})
|
|
151
|
+
}]
|
|
152
|
+
})
|
|
153
|
+
}, e.id))
|
|
154
|
+
})
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ j(s, {
|
|
157
|
+
title: N("ggnomad.travelPolicy.policiesTitle", "Travel policies"),
|
|
158
|
+
children: X.length === 0 ? /* @__PURE__ */ j(k, {
|
|
159
|
+
illustration: "empty-data",
|
|
160
|
+
title: N("ggnomad.travelPolicy.policiesEmptyTitle", "No travel policy defined yet"),
|
|
161
|
+
description: N("ggnomad.travelPolicy.policiesEmptyDescription", "Create one to enforce budget caps, blocked destinations, and class rules on future bookings.")
|
|
162
|
+
}) : /* @__PURE__ */ j("div", {
|
|
163
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
164
|
+
children: X.map((e) => /* @__PURE__ */ j(c, {
|
|
165
|
+
title: e.name,
|
|
166
|
+
meta: e.costCenter ? N("ggnomad.travelPolicy.card.costCenter", "Cost center: {{cc}}").replace("{{cc}}", e.costCenter) : N("ggnomad.travelPolicy.card.workspaceWide", "Workspace-wide default"),
|
|
167
|
+
price: e.maxBookingAmount == null ? N("ggnomad.travelPolicy.card.noCap", "No per-booking cap") : `${e.currency} ${Number(e.maxBookingAmount).toFixed(2)} / booking`,
|
|
168
|
+
badge: /* @__PURE__ */ j(o, {
|
|
169
|
+
status: e.isActive ? "ACTIVE" : "PAUSED",
|
|
170
|
+
live: e.isActive
|
|
171
|
+
}),
|
|
172
|
+
footer: /* @__PURE__ */ j(O, {
|
|
173
|
+
align: "end",
|
|
174
|
+
actions: [{
|
|
175
|
+
label: N("ggnomad.travelPolicy.actions.deletePolicy", "Remove policy"),
|
|
176
|
+
onSelect: () => B(e),
|
|
177
|
+
intent: "destructive",
|
|
178
|
+
icon: /* @__PURE__ */ j(g, {})
|
|
179
|
+
}]
|
|
180
|
+
})
|
|
181
|
+
}, e.id))
|
|
182
|
+
})
|
|
183
|
+
}),
|
|
184
|
+
/* @__PURE__ */ j(ne, {
|
|
185
|
+
open: oe,
|
|
186
|
+
onOpenChange: P,
|
|
187
|
+
onCreated: () => void Y()
|
|
188
|
+
}),
|
|
189
|
+
/* @__PURE__ */ j(b, {
|
|
190
|
+
open: F !== null,
|
|
191
|
+
onOpenChange: (e) => {
|
|
192
|
+
e || (I(null), R(""));
|
|
193
|
+
},
|
|
194
|
+
children: /* @__PURE__ */ M(C, { children: [
|
|
195
|
+
/* @__PURE__ */ M(E, { children: [/* @__PURE__ */ j(D, { children: N("ggnomad.travelPolicy.rejectDialog.title", "Reject this booking?") }), /* @__PURE__ */ j(w, { children: N("ggnomad.travelPolicy.rejectDialog.body", "This cancels {{ref}} and revokes any issued ticket. The traveler sees your reason.").replace("{{ref}}", F?.booking?.referenceCode ?? F?.bookingId ?? "") })] }),
|
|
196
|
+
/* @__PURE__ */ M("div", { children: [/* @__PURE__ */ j("label", {
|
|
197
|
+
htmlFor: "reject-reason",
|
|
198
|
+
className: "block text-sm font-medium text-text-primary",
|
|
199
|
+
children: N("ggnomad.travelPolicy.rejectDialog.reasonLabel", "Reason")
|
|
200
|
+
}), /* @__PURE__ */ j("textarea", {
|
|
201
|
+
id: "reject-reason",
|
|
202
|
+
value: L,
|
|
203
|
+
onChange: (e) => R(e.target.value),
|
|
204
|
+
disabled: Z,
|
|
205
|
+
rows: 3,
|
|
206
|
+
className: "mt-1 w-full rounded-md border border-border-seam bg-bg-canvas px-3 py-2 text-sm text-text-primary placeholder-text-secondary focus:border-action-primary-bg focus:outline-none focus:ring-1 focus:ring-action-primary-bg disabled:opacity-50",
|
|
207
|
+
placeholder: N("ggnomad.travelPolicy.rejectDialog.reasonPlaceholder", "e.g. Over quarterly budget")
|
|
208
|
+
})] }),
|
|
209
|
+
/* @__PURE__ */ M(T, { children: [/* @__PURE__ */ j(S, {
|
|
210
|
+
disabled: Z,
|
|
211
|
+
children: N("ggnomad.travelPolicy.rejectDialog.cancel", "Keep pending")
|
|
212
|
+
}), /* @__PURE__ */ j(x, {
|
|
213
|
+
className: "bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover",
|
|
214
|
+
disabled: Z,
|
|
215
|
+
onClick: (e) => {
|
|
216
|
+
e.preventDefault(), fe();
|
|
217
|
+
},
|
|
218
|
+
children: Z ? N("ggnomad.travelPolicy.rejectDialog.rejecting", "Rejecting…") : N("ggnomad.travelPolicy.rejectDialog.confirm", "Reject booking")
|
|
219
|
+
})] })
|
|
220
|
+
] })
|
|
221
|
+
}),
|
|
222
|
+
/* @__PURE__ */ j(b, {
|
|
223
|
+
open: z !== null,
|
|
224
|
+
onOpenChange: (e) => {
|
|
225
|
+
e || B(null);
|
|
226
|
+
},
|
|
227
|
+
children: /* @__PURE__ */ M(C, { children: [/* @__PURE__ */ M(E, { children: [/* @__PURE__ */ j(D, { children: N("ggnomad.travelPolicy.deleteDialog.title", "Remove this policy?") }), /* @__PURE__ */ j(w, { children: N("ggnomad.travelPolicy.deleteDialog.body", "Bookings already approved or rejected under {{name}} are unaffected. New bookings stop being checked against it.").replace("{{name}}", z?.name ?? "") })] }), /* @__PURE__ */ M(T, { children: [/* @__PURE__ */ j(S, {
|
|
228
|
+
disabled: Q,
|
|
229
|
+
children: N("ggnomad.travelPolicy.deleteDialog.cancel", "Keep policy")
|
|
230
|
+
}), /* @__PURE__ */ j(x, {
|
|
231
|
+
className: "bg-action-danger-bg text-action-danger-text hover:bg-action-danger-bgHover",
|
|
232
|
+
disabled: Q,
|
|
233
|
+
onClick: (e) => {
|
|
234
|
+
e.preventDefault(), pe();
|
|
235
|
+
},
|
|
236
|
+
children: Q ? N("ggnomad.travelPolicy.deleteDialog.removing", "Removing…") : N("ggnomad.travelPolicy.deleteDialog.confirm", "Remove policy")
|
|
237
|
+
})] })] })
|
|
238
|
+
})
|
|
239
|
+
]
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
export { N as TravelPolicyPage };
|
|
244
|
+
|
|
245
|
+
//# sourceMappingURL=TravelPolicyPage.js.map
|