@voyantjs/allocation-ui 0.37.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.
@@ -0,0 +1,66 @@
1
+ import type { AllocationManifestTraveler, AllocationResource } from "@voyantjs/availability-react";
2
+ import type { AllocationUiMessages } from "../i18n/index.js";
3
+ export declare const ROOM_KIND = "room";
4
+ export declare const VEHICLE_KIND = "vehicle";
5
+ export declare const VEHICLE_SEAT_KIND = "vehicle_seat";
6
+ export declare const PARENT_ONLY_KINDS: Set<string>;
7
+ export type AllocationOccupants = {
8
+ byResource: Map<string, AllocationManifestTraveler[]>;
9
+ byTravelerId: Map<string, AllocationManifestTraveler>;
10
+ unallocated: AllocationManifestTraveler[];
11
+ };
12
+ export type ValidationIssue = {
13
+ id: string;
14
+ label: string;
15
+ };
16
+ export declare function collectOccupants(travelers: AllocationManifestTraveler[], resources: AllocationResource[], kind: string): AllocationOccupants;
17
+ export declare function buildValidationIssues({ travelers, resources, occupants, kind, messages, }: {
18
+ travelers: AllocationManifestTraveler[];
19
+ resources: AllocationResource[];
20
+ occupants: AllocationOccupants;
21
+ kind: string;
22
+ messages: AllocationUiMessages;
23
+ }): ValidationIssue[];
24
+ export declare function splitSharingGroups(travelers: AllocationManifestTraveler[], kind: string): string[];
25
+ export declare function groupSeatsByVehicle(seats: AllocationResource[], vehicles: AllocationResource[], messages: AllocationUiMessages): {
26
+ seats: {
27
+ id: string;
28
+ slotId: string;
29
+ kind: string;
30
+ refType: string | null;
31
+ refId: string | null;
32
+ label: string | null;
33
+ capacity: number;
34
+ flags: Record<string, unknown>;
35
+ parentId: string | null;
36
+ sortOrder: number;
37
+ createdAt: string | Date;
38
+ updatedAt: string | Date;
39
+ }[];
40
+ id: string;
41
+ label: string;
42
+ sortOrder: number;
43
+ }[];
44
+ export declare function seatRows(seats: AllocationResource[]): {
45
+ rowKey: string;
46
+ seats: {
47
+ id: string;
48
+ slotId: string;
49
+ kind: string;
50
+ refType: string | null;
51
+ refId: string | null;
52
+ label: string | null;
53
+ capacity: number;
54
+ flags: Record<string, unknown>;
55
+ parentId: string | null;
56
+ sortOrder: number;
57
+ createdAt: string | Date;
58
+ updatedAt: string | Date;
59
+ }[];
60
+ }[];
61
+ export declare function seatName(seat: AllocationResource, messages: AllocationUiMessages): string;
62
+ export declare function parentKindFor(kind: string): "" | "vehicle";
63
+ export declare function defaultCapacityFor(kind: string): 1 | 2;
64
+ export declare function kindLabel(kind: string, messages: AllocationUiMessages): string;
65
+ export declare function flagString(value: unknown): string | null;
66
+ //# sourceMappingURL=slot-allocation-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-allocation-model.d.ts","sourceRoot":"","sources":["../../src/components/slot-allocation-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AAElG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAE5D,eAAO,MAAM,SAAS,SAAS,CAAA;AAC/B,eAAO,MAAM,YAAY,YAAY,CAAA;AACrC,eAAO,MAAM,iBAAiB,iBAAiB,CAAA;AAC/C,eAAO,MAAM,iBAAiB,aAA0B,CAAA;AAExD,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,0BAA0B,EAAE,CAAC,CAAA;IACrD,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAA;IACrD,WAAW,EAAE,0BAA0B,EAAE,CAAA;CAC1C,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,0BAA0B,EAAE,EACvC,SAAS,EAAE,kBAAkB,EAAE,EAC/B,IAAI,EAAE,MAAM,GACX,mBAAmB,CAmBrB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,SAAS,EACT,SAAS,EACT,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,SAAS,EAAE,0BAA0B,EAAE,CAAA;IACvC,SAAS,EAAE,kBAAkB,EAAE,CAAA;IAC/B,SAAS,EAAE,mBAAmB,CAAA;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,oBAAoB,CAAA;CAC/B,qBA4BA;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,0BAA0B,EAAE,EAAE,IAAI,EAAE,MAAM,YAevF;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,kBAAkB,EAAE,EAC3B,QAAQ,EAAE,kBAAkB,EAAE,EAC9B,QAAQ,EAAE,oBAAoB;;;;;;;;;;;;;;;QAKtB,MAAM;WAAS,MAAM;eAAa,MAAM;IA0BjD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,kBAAkB,EAAE;;;;;;;;;;;;;;;;IAenD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,kBAAkB,EAAE,QAAQ,EAAE,oBAAoB,UAKhF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,kBAEzC;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,SAE9C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,UAMrE;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,iBAExC"}
@@ -0,0 +1,154 @@
1
+ export const ROOM_KIND = "room";
2
+ export const VEHICLE_KIND = "vehicle";
3
+ export const VEHICLE_SEAT_KIND = "vehicle_seat";
4
+ export const PARENT_ONLY_KINDS = new Set([VEHICLE_KIND]);
5
+ export function collectOccupants(travelers, resources, kind) {
6
+ const resourceIds = new Set(resources.map((resource) => resource.id));
7
+ const byResource = new Map();
8
+ const byTravelerId = new Map();
9
+ const unallocated = [];
10
+ for (const traveler of travelers) {
11
+ byTravelerId.set(traveler.id, traveler);
12
+ const resourceId = traveler.allocations[kind];
13
+ if (!resourceId || !resourceIds.has(resourceId)) {
14
+ unallocated.push(traveler);
15
+ continue;
16
+ }
17
+ const list = byResource.get(resourceId) ?? [];
18
+ list.push(traveler);
19
+ byResource.set(resourceId, list);
20
+ }
21
+ return { byResource, byTravelerId, unallocated };
22
+ }
23
+ export function buildValidationIssues({ travelers, resources, occupants, kind, messages, }) {
24
+ const issues = [];
25
+ if (occupants.unallocated.length > 0) {
26
+ issues.push({
27
+ id: "unallocated",
28
+ label: `${occupants.unallocated.length} ${messages.validationUnallocated}`,
29
+ });
30
+ }
31
+ for (const resource of resources) {
32
+ const count = occupants.byResource.get(resource.id)?.length ?? 0;
33
+ if (count > resource.capacity) {
34
+ issues.push({
35
+ id: `over-capacity:${resource.id}`,
36
+ label: `${resource.label ?? kindLabel(kind, messages)} ${messages.validationOverCapacity}`,
37
+ });
38
+ }
39
+ }
40
+ for (const splitGroup of splitSharingGroups(travelers, kind)) {
41
+ issues.push({
42
+ id: `split:${splitGroup}`,
43
+ label: `${messages.validationSplitGroup}: ${splitGroup}`,
44
+ });
45
+ }
46
+ return issues;
47
+ }
48
+ export function splitSharingGroups(travelers, kind) {
49
+ const allocationsByGroup = new Map();
50
+ for (const traveler of travelers) {
51
+ const groupId = traveler.sharingGroupId;
52
+ if (!groupId)
53
+ continue;
54
+ const allocations = allocationsByGroup.get(groupId) ?? new Set();
55
+ allocations.add(traveler.allocations[kind] ?? "unallocated");
56
+ allocationsByGroup.set(groupId, allocations);
57
+ }
58
+ const split = [];
59
+ for (const [groupId, allocations] of allocationsByGroup) {
60
+ if (allocations.size > 1)
61
+ split.push(groupId);
62
+ }
63
+ return split;
64
+ }
65
+ export function groupSeatsByVehicle(seats, vehicles, messages) {
66
+ const vehiclesById = new Map(vehicles.map((vehicle) => [vehicle.id, vehicle]));
67
+ const grouped = new Map();
68
+ for (const seat of seats) {
69
+ const parentId = seat.parentId ?? "ungrouped";
70
+ const parent = parentId === "ungrouped" ? null : vehiclesById.get(parentId);
71
+ const group = grouped.get(parentId) ??
72
+ grouped
73
+ .set(parentId, {
74
+ id: parentId,
75
+ label: parent?.label ?? messages.vehicle,
76
+ sortOrder: parent?.sortOrder ?? 0,
77
+ seats: [],
78
+ })
79
+ .get(parentId);
80
+ group?.seats.push(seat);
81
+ }
82
+ return Array.from(grouped.values())
83
+ .map((group) => ({
84
+ ...group,
85
+ seats: group.seats.sort(compareSeatResources),
86
+ }))
87
+ .sort((a, b) => a.sortOrder - b.sortOrder || a.label.localeCompare(b.label));
88
+ }
89
+ export function seatRows(seats) {
90
+ const byRow = new Map();
91
+ for (const seat of seats) {
92
+ const rowKey = String(flagNumber(seat.flags.row) ?? 0);
93
+ const row = byRow.get(rowKey) ?? [];
94
+ row.push(seat);
95
+ byRow.set(rowKey, row);
96
+ }
97
+ return Array.from(byRow.entries())
98
+ .map(([rowKey, rowSeats]) => ({
99
+ rowKey,
100
+ seats: rowSeats.sort(compareSeatResources),
101
+ }))
102
+ .sort((a, b) => Number(a.rowKey) - Number(b.rowKey) || a.rowKey.localeCompare(b.rowKey));
103
+ }
104
+ export function seatName(seat, messages) {
105
+ const row = flagNumber(seat.flags.row);
106
+ const column = flagString(seat.flags.column);
107
+ if (row && column)
108
+ return `${row}${column}`;
109
+ return seat.label ?? messages.seat;
110
+ }
111
+ export function parentKindFor(kind) {
112
+ return kind === VEHICLE_SEAT_KIND ? VEHICLE_KIND : "";
113
+ }
114
+ export function defaultCapacityFor(kind) {
115
+ return kind === VEHICLE_SEAT_KIND ? 1 : kind === ROOM_KIND ? 2 : 1;
116
+ }
117
+ export function kindLabel(kind, messages) {
118
+ if (kind === ROOM_KIND)
119
+ return messages.rooms;
120
+ if (kind === VEHICLE_SEAT_KIND)
121
+ return messages.vehicleSeats;
122
+ if (kind === "cabin")
123
+ return messages.cabins;
124
+ if (kind === "flight_seat")
125
+ return messages.flightSeats;
126
+ return titleCaseKind(kind);
127
+ }
128
+ export function flagString(value) {
129
+ return typeof value === "string" ? value : null;
130
+ }
131
+ function compareSeatResources(a, b) {
132
+ const aRow = flagNumber(a.flags.row) ?? 0;
133
+ const bRow = flagNumber(b.flags.row) ?? 0;
134
+ const aColumn = flagString(a.flags.column) ?? "";
135
+ const bColumn = flagString(b.flags.column) ?? "";
136
+ return aRow - bRow || aColumn.localeCompare(bColumn) || a.sortOrder - b.sortOrder;
137
+ }
138
+ function titleCaseKind(kind) {
139
+ return kind
140
+ .split(/[_-]/g)
141
+ .filter(Boolean)
142
+ .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
143
+ .join(" ");
144
+ }
145
+ function flagNumber(value) {
146
+ if (typeof value === "number" && Number.isFinite(value))
147
+ return value;
148
+ if (typeof value === "string") {
149
+ const parsed = Number(value);
150
+ if (Number.isFinite(parsed))
151
+ return parsed;
152
+ }
153
+ return null;
154
+ }
@@ -0,0 +1,14 @@
1
+ import { type AllocationManifestTraveler } from "@voyantjs/availability-react";
2
+ import { type ReactNode } from "react";
3
+ export interface SlotAllocationPageProps {
4
+ slotId: string;
5
+ className?: string;
6
+ onBack?: () => void;
7
+ renderExtraActions?: (context: {
8
+ slotId: string;
9
+ kind: string;
10
+ }) => ReactNode;
11
+ renderTravelerActions?: (traveler: AllocationManifestTraveler) => ReactNode;
12
+ }
13
+ export declare function SlotAllocationPage({ slotId, className, onBack, renderExtraActions, renderTravelerActions, }: SlotAllocationPageProps): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=slot-allocation-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-allocation-page.d.ts","sourceRoot":"","sources":["../../src/components/slot-allocation-page.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,0BAA0B,EAOhC,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EAAkB,KAAK,SAAS,EAAqB,MAAM,OAAO,CAAA;AAiBzE,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAA;IAC7E,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,0BAA0B,KAAK,SAAS,CAAA;CAC5E;AAED,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,SAAS,EACT,MAAM,EACN,kBAAkB,EAClB,qBAAqB,GACtB,EAAE,uBAAuB,2CA6TzB"}
@@ -0,0 +1,147 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useAllocationAutomationMutation, useAllocationResourceMutation, useAssignTravelerAllocationMutation, useProductResourceTemplates, useSlotAllocation, useSlotAllocationAuditLog, } from "@voyantjs/availability-react";
4
+ import { Button, cn, Input, Label, Tabs, TabsList, TabsTrigger } from "@voyantjs/ui/components";
5
+ import { Armchair, ArrowLeft, Bed, Download, Plus, Sparkles, Users, Wand2 } from "lucide-react";
6
+ import { useMemo, useState } from "react";
7
+ import { useAllocationUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { buildValidationIssues, collectOccupants, defaultCapacityFor, kindLabel, PARENT_ONLY_KINDS, parentKindFor, ROOM_KIND, VEHICLE_SEAT_KIND, } from "./slot-allocation-model.js";
9
+ import { ResourceColumnsView } from "./slot-allocation-resource-view.js";
10
+ import { VehicleSeatsView } from "./slot-allocation-seat-view.js";
11
+ import { AuditLogCard, ValidationSummary } from "./slot-allocation-shared.js";
12
+ export function SlotAllocationPage({ slotId, className, onBack, renderExtraActions, renderTravelerActions, }) {
13
+ const messages = useAllocationUiMessagesOrDefault();
14
+ const allocation = useSlotAllocation({ slotId });
15
+ const auditLog = useSlotAllocationAuditLog({ slotId });
16
+ const resourceMutation = useAllocationResourceMutation(slotId);
17
+ const assignMutation = useAssignTravelerAllocationMutation(slotId);
18
+ const automationMutation = useAllocationAutomationMutation(slotId);
19
+ const [selectedKind, setSelectedKind] = useState(ROOM_KIND);
20
+ const [addingResource, setAddingResource] = useState(false);
21
+ const [resourceLabel, setResourceLabel] = useState("");
22
+ const [resourceCapacity, setResourceCapacity] = useState(2);
23
+ const [error, setError] = useState(null);
24
+ const data = allocation.data?.data;
25
+ const templates = useProductResourceTemplates({
26
+ productId: data?.slot.productId,
27
+ enabled: Boolean(data?.slot.productId),
28
+ });
29
+ const travelers = useMemo(() => {
30
+ const out = [];
31
+ for (const booking of data?.bookings ?? []) {
32
+ if (booking.status === "cancelled")
33
+ continue;
34
+ out.push(...booking.travelers);
35
+ }
36
+ return out;
37
+ }, [data?.bookings]);
38
+ const allocationKinds = useMemo(() => {
39
+ const kinds = [];
40
+ const addKind = (kind) => {
41
+ if (!kind || PARENT_ONLY_KINDS.has(kind) || kinds.includes(kind))
42
+ return;
43
+ kinds.push(kind);
44
+ };
45
+ addKind(ROOM_KIND);
46
+ for (const resource of data?.resources ?? [])
47
+ addKind(resource.kind);
48
+ for (const option of templates.data?.data ?? []) {
49
+ for (const template of option.templates)
50
+ addKind(template.kind);
51
+ }
52
+ return kinds;
53
+ }, [data?.resources, templates.data?.data]);
54
+ const activeKind = allocationKinds.includes(selectedKind)
55
+ ? selectedKind
56
+ : (allocationKinds[0] ?? ROOM_KIND);
57
+ const resources = useMemo(() => (data?.resources ?? []).filter((resource) => resource.kind === activeKind), [data?.resources, activeKind]);
58
+ const parentResources = useMemo(() => (data?.resources ?? []).filter((resource) => resource.kind === parentKindFor(activeKind)), [data?.resources, activeKind]);
59
+ const occupants = useMemo(() => collectOccupants(travelers, resources, activeKind), [travelers, resources, activeKind]);
60
+ const validationIssues = useMemo(() => buildValidationIssues({ travelers, resources, occupants, kind: activeKind, messages }), [travelers, resources, occupants, activeKind, messages]);
61
+ function downloadExport(kind) {
62
+ globalThis.location.assign(`/v1/admin/availability/slots/${encodeURIComponent(slotId)}/allocation/export-${kind}`);
63
+ }
64
+ async function assignTraveler(travelerId, resourceId) {
65
+ setError(null);
66
+ try {
67
+ await assignMutation.mutateAsync({ travelerId, kind: activeKind, resourceId });
68
+ }
69
+ catch (err) {
70
+ setError(err instanceof Error ? err.message : messages.allocationFailed);
71
+ }
72
+ }
73
+ async function swapOrAssignSeat(travelerId, resourceId) {
74
+ const traveler = occupants.byTravelerId.get(travelerId);
75
+ if (!traveler)
76
+ return;
77
+ const currentResourceId = traveler.allocations[activeKind] ?? null;
78
+ if (currentResourceId === resourceId)
79
+ return;
80
+ setError(null);
81
+ try {
82
+ const targetOccupant = (occupants.byResource.get(resourceId) ?? []).find((occupant) => occupant.id !== travelerId);
83
+ if (targetOccupant) {
84
+ await assignMutation.mutateAsync({
85
+ travelerId: targetOccupant.id,
86
+ kind: activeKind,
87
+ resourceId: currentResourceId,
88
+ });
89
+ }
90
+ await assignMutation.mutateAsync({ travelerId, kind: activeKind, resourceId });
91
+ }
92
+ catch (err) {
93
+ setError(err instanceof Error ? err.message : messages.allocationFailed);
94
+ }
95
+ }
96
+ async function createResource(event) {
97
+ event.preventDefault();
98
+ setError(null);
99
+ try {
100
+ await resourceMutation.create.mutateAsync({
101
+ kind: activeKind,
102
+ label: resourceLabel.trim() || null,
103
+ capacity: resourceCapacity,
104
+ });
105
+ setResourceLabel("");
106
+ setResourceCapacity(defaultCapacityFor(activeKind));
107
+ setAddingResource(false);
108
+ }
109
+ catch (err) {
110
+ setError(err instanceof Error ? err.message : messages.createResourceFailed);
111
+ }
112
+ }
113
+ async function generateResources() {
114
+ setError(null);
115
+ try {
116
+ await automationMutation.autoMaterialize.mutateAsync({ kind: activeKind });
117
+ }
118
+ catch (err) {
119
+ setError(err instanceof Error ? err.message : messages.generateResourcesFailed);
120
+ }
121
+ }
122
+ async function autoAllocate() {
123
+ setError(null);
124
+ try {
125
+ await automationMutation.autoAllocate.mutateAsync({ kind: activeKind });
126
+ }
127
+ catch (err) {
128
+ setError(err instanceof Error ? err.message : messages.autoAllocateFailed);
129
+ }
130
+ }
131
+ if (allocation.isPending) {
132
+ return (_jsx("div", { className: cn("p-6 text-sm text-muted-foreground", className), children: messages.loading }));
133
+ }
134
+ if (!data || travelers.length === 0) {
135
+ return (_jsxs("div", { className: cn("flex flex-col items-center justify-center gap-3 p-8", className), children: [_jsx(Users, { className: "size-6 text-muted-foreground", "aria-hidden": "true" }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.empty })] }));
136
+ }
137
+ const isSeatMap = activeKind === VEHICLE_SEAT_KIND;
138
+ const canManuallyAddResource = !isSeatMap;
139
+ return (_jsxs("div", { className: cn("flex flex-col gap-4 p-6", className), children: [_jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-start md:justify-between", children: [_jsxs("div", { className: "flex items-start gap-3", children: [onBack ? (_jsx(Button, { variant: "ghost", size: "icon", onClick: onBack, "aria-label": messages.back, children: _jsx(ArrowLeft, { "data-icon": true, "aria-hidden": "true" }) })) : null, _jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: messages.pageTitle }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2 text-sm text-muted-foreground", children: [_jsxs("span", { children: [data.summary.travelerCount, " ", messages.travelers] }), _jsxs("span", { children: [resources.length, " ", kindLabel(activeKind, messages).toLowerCase()] })] })] })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [renderExtraActions?.({ slotId, kind: activeKind }), _jsxs(Button, { variant: "outline", onClick: () => downloadExport("passengers"), children: [_jsx(Download, { "data-icon": "inline-start", "aria-hidden": "true" }), messages.exportPassengers] }), _jsxs(Button, { variant: "outline", onClick: () => downloadExport("rooming-list"), children: [_jsx(Download, { "data-icon": "inline-start", "aria-hidden": "true" }), messages.exportRooming] }), resources.length === 0 ? (_jsxs(Button, { variant: "outline", onClick: () => void generateResources(), disabled: automationMutation.autoMaterialize.isPending, children: [_jsx(Sparkles, { "data-icon": "inline-start", "aria-hidden": "true" }), automationMutation.autoMaterialize.isPending
140
+ ? messages.generatingResources
141
+ : messages.generateResources] })) : (_jsxs(Button, { variant: "outline", onClick: () => void autoAllocate(), disabled: automationMutation.autoAllocate.isPending, children: [_jsx(Wand2, { "data-icon": "inline-start", "aria-hidden": "true" }), automationMutation.autoAllocate.isPending
142
+ ? messages.autoAllocating
143
+ : messages.autoAllocate] })), canManuallyAddResource ? (_jsxs(Button, { variant: "outline", onClick: () => {
144
+ setResourceCapacity(defaultCapacityFor(activeKind));
145
+ setAddingResource((value) => !value);
146
+ }, children: [_jsx(Plus, { "data-icon": "inline-start", "aria-hidden": "true" }), messages.addResource] })) : null] })] }), _jsx(Tabs, { value: activeKind, onValueChange: setSelectedKind, children: _jsx(TabsList, { className: "flex h-auto w-fit flex-wrap justify-start", children: allocationKinds.map((kind) => (_jsxs(TabsTrigger, { value: kind, className: "gap-2", children: [kind === VEHICLE_SEAT_KIND ? (_jsx(Armchair, { className: "size-4", "aria-hidden": "true" })) : (_jsx(Bed, { className: "size-4", "aria-hidden": "true" })), kindLabel(kind, messages)] }, kind))) }) }), error ? (_jsx("div", { className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: error })) : null, addingResource && canManuallyAddResource ? (_jsxs("form", { className: "grid gap-3 rounded-md border bg-muted/30 p-3 sm:grid-cols-[1fr_8rem_auto_auto]", onSubmit: createResource, children: [_jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: "allocation-resource-label", children: messages.resourceLabel }), _jsx(Input, { id: "allocation-resource-label", value: resourceLabel, onChange: (event) => setResourceLabel(event.target.value), placeholder: activeKind === ROOM_KIND ? "102" : kindLabel(activeKind, messages) })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: "allocation-resource-capacity", children: messages.resourceCapacity }), _jsx(Input, { id: "allocation-resource-capacity", type: "number", min: 1, value: resourceCapacity, onChange: (event) => setResourceCapacity(Number(event.target.value) || 1) })] }), _jsx(Button, { type: "submit", className: "self-end", disabled: resourceMutation.create.isPending, children: messages.createResource }), _jsx(Button, { type: "button", variant: "ghost", className: "self-end", onClick: () => setAddingResource(false), children: messages.cancel })] })) : null, _jsx(ValidationSummary, { issues: validationIssues, resources: resources, unallocatedCount: occupants.unallocated.length }), isSeatMap ? (_jsx(VehicleSeatsView, { seats: resources, vehicles: parentResources, occupants: occupants, sharingGroupLabels: data.sharingGroupLabels, onDropTraveler: (travelerId, resourceId) => void swapOrAssignSeat(travelerId, resourceId), onUnassignTraveler: (travelerId) => void assignTraveler(travelerId, null), renderTravelerActions: renderTravelerActions })) : (_jsx(ResourceColumnsView, { kind: activeKind, resources: resources, travelers: travelers, occupants: occupants, sharingGroupLabels: data.sharingGroupLabels, onDropTraveler: (travelerId, resourceId) => void assignTraveler(travelerId, resourceId), onRemoveResource: (resourceId) => void resourceMutation.remove.mutateAsync(resourceId), renderTravelerActions: renderTravelerActions })), _jsx(AuditLogCard, { entries: auditLog.data?.data ?? [] })] }));
147
+ }
@@ -0,0 +1,14 @@
1
+ import type { AllocationManifestTraveler, AllocationResource } from "@voyantjs/availability-react";
2
+ import type { ReactNode } from "react";
3
+ import { type AllocationOccupants } from "./slot-allocation-model.js";
4
+ export declare function ResourceColumnsView({ kind, resources, travelers, occupants, sharingGroupLabels, onDropTraveler, onRemoveResource, renderTravelerActions, }: {
5
+ kind: string;
6
+ resources: AllocationResource[];
7
+ travelers: AllocationManifestTraveler[];
8
+ occupants: AllocationOccupants;
9
+ sharingGroupLabels: Record<string, string>;
10
+ onDropTraveler: (travelerId: string, resourceId: string | null) => void;
11
+ onRemoveResource: (resourceId: string) => void;
12
+ renderTravelerActions?: (traveler: AllocationManifestTraveler) => ReactNode;
13
+ }): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=slot-allocation-resource-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-allocation-resource-view.d.ts","sourceRoot":"","sources":["../../src/components/slot-allocation-resource-view.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AAGlG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,EAAE,KAAK,mBAAmB,EAAgC,MAAM,4BAA4B,CAAA;AAGnG,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,SAAS,EACT,SAAS,EACT,SAAS,EACT,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,qBAAqB,GACtB,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,kBAAkB,EAAE,CAAA;IAC/B,SAAS,EAAE,0BAA0B,EAAE,CAAA;IACvC,SAAS,EAAE,mBAAmB,CAAA;IAC9B,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACvE,gBAAgB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9C,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,0BAA0B,KAAK,SAAS,CAAA;CAC5E,2CAgDA"}
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge, Button } from "@voyantjs/ui/components";
4
+ import { Armchair, Bed, Trash2, Users } from "lucide-react";
5
+ import { useAllocationUiMessagesOrDefault } from "../i18n/index.js";
6
+ import { kindLabel, VEHICLE_SEAT_KIND } from "./slot-allocation-model.js";
7
+ import { DropColumn, ResourceFlagBadges, TravelerTile } from "./slot-allocation-shared.js";
8
+ export function ResourceColumnsView({ kind, resources, travelers, occupants, sharingGroupLabels, onDropTraveler, onRemoveResource, renderTravelerActions, }) {
9
+ const messages = useAllocationUiMessagesOrDefault();
10
+ return (_jsxs("div", { className: "grid gap-4 xl:grid-cols-[minmax(18rem,22rem)_1fr]", children: [_jsx(DropColumn, { id: "unallocated", icon: _jsx(Users, { className: "size-4", "aria-hidden": "true" }), title: messages.unallocated, description: messages.unallocatedDescription, count: occupants.unallocated.length, capacity: travelers.length, onDropTraveler: (travelerId) => onDropTraveler(travelerId, null), children: occupants.unallocated.map((traveler) => (_jsx(TravelerTile, { traveler: traveler, sharingGroupLabel: traveler.sharingGroupId ? sharingGroupLabels[traveler.sharingGroupId] : null, renderActions: renderTravelerActions }, traveler.id))) }), _jsx("div", { className: "grid min-w-0 gap-4 md:grid-cols-2 2xl:grid-cols-3", children: resources.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-sm text-muted-foreground", children: messages.noResources })) : (resources.map((resource) => (_jsx(AllocationResourceColumn, { kind: kind, resource: resource, occupants: occupants.byResource.get(resource.id) ?? [], sharingGroupLabels: sharingGroupLabels, onDropTraveler: (travelerId) => onDropTraveler(travelerId, resource.id), onRemoveResource: () => onRemoveResource(resource.id), renderTravelerActions: renderTravelerActions }, resource.id)))) })] }));
11
+ }
12
+ function AllocationResourceColumn({ kind, resource, occupants, onDropTraveler, onRemoveResource, sharingGroupLabels, renderTravelerActions, }) {
13
+ const messages = useAllocationUiMessagesOrDefault();
14
+ const full = occupants.length >= resource.capacity;
15
+ return (_jsxs(DropColumn, { id: `allocation-resource:${resource.id}`, icon: kind === VEHICLE_SEAT_KIND ? _jsx(Armchair, { className: "size-4" }) : _jsx(Bed, { className: "size-4" }), title: resource.label ?? kindLabel(kind, messages), description: `${messages.capacity}: ${occupants.length}/${resource.capacity}`, count: occupants.length, capacity: resource.capacity, disabled: full, onDropTraveler: onDropTraveler, action: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: onRemoveResource, children: _jsx(Trash2, { className: "size-4", "aria-hidden": "true" }) }), children: [_jsx(ResourceFlagBadges, { resource: resource }), full ? (_jsx(Badge, { variant: "secondary", className: "w-fit", children: messages.overCapacity })) : null, occupants.map((traveler) => (_jsx(TravelerTile, { traveler: traveler, sharingGroupLabel: traveler.sharingGroupId ? sharingGroupLabels[traveler.sharingGroupId] : null, renderActions: renderTravelerActions }, traveler.id))), !full ? (_jsx("div", { className: "rounded-md border border-dashed p-3 text-xs text-muted-foreground", children: messages.dropHere })) : null] }));
16
+ }
@@ -0,0 +1,13 @@
1
+ import type { AllocationManifestTraveler, AllocationResource } from "@voyantjs/availability-react";
2
+ import { type ReactNode } from "react";
3
+ import { type AllocationOccupants } from "./slot-allocation-model.js";
4
+ export declare function VehicleSeatsView({ seats, vehicles, occupants, sharingGroupLabels, onDropTraveler, onUnassignTraveler, renderTravelerActions, }: {
5
+ seats: AllocationResource[];
6
+ vehicles: AllocationResource[];
7
+ occupants: AllocationOccupants;
8
+ sharingGroupLabels: Record<string, string>;
9
+ onDropTraveler: (travelerId: string, resourceId: string) => void;
10
+ onUnassignTraveler: (travelerId: string) => void;
11
+ renderTravelerActions?: (traveler: AllocationManifestTraveler) => ReactNode;
12
+ }): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=slot-allocation-seat-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-allocation-seat-view.d.ts","sourceRoot":"","sources":["../../src/components/slot-allocation-seat-view.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AAGlG,OAAO,EAAkB,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAGhE,OAAO,EACL,KAAK,mBAAmB,EAIzB,MAAM,4BAA4B,CAAA;AAGnC,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,SAAS,EACT,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,qBAAqB,GACtB,EAAE;IACD,KAAK,EAAE,kBAAkB,EAAE,CAAA;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,SAAS,EAAE,mBAAmB,CAAA;IAC9B,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAChE,kBAAkB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;IAChD,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,0BAA0B,KAAK,SAAS,CAAA;CAC5E,2CAsFA"}
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge, Card, CardContent, CardHeader, CardTitle, cn } from "@voyantjs/ui/components";
4
+ import { Armchair, Crown, Users } from "lucide-react";
5
+ import { useState } from "react";
6
+ import { useAllocationUiMessagesOrDefault } from "../i18n/index.js";
7
+ import { groupSeatsByVehicle, seatName, seatRows, } from "./slot-allocation-model.js";
8
+ import { DropColumn, SeatPositionBadge, TravelerTile } from "./slot-allocation-shared.js";
9
+ export function VehicleSeatsView({ seats, vehicles, occupants, sharingGroupLabels, onDropTraveler, onUnassignTraveler, renderTravelerActions, }) {
10
+ const messages = useAllocationUiMessagesOrDefault();
11
+ const groups = groupSeatsByVehicle(seats, vehicles, messages);
12
+ return (_jsxs("div", { className: "grid gap-4 xl:grid-cols-[minmax(18rem,22rem)_1fr]", children: [_jsx(DropColumn, { id: "unallocated", icon: _jsx(Users, { className: "size-4", "aria-hidden": "true" }), title: messages.unallocated, description: messages.unallocatedDescription, count: occupants.unallocated.length, capacity: occupants.byTravelerId.size, onDropTraveler: onUnassignTraveler, children: occupants.unallocated.map((traveler) => (_jsx(TravelerTile, { traveler: traveler, sharingGroupLabel: traveler.sharingGroupId ? sharingGroupLabels[traveler.sharingGroupId] : null, renderActions: renderTravelerActions }, traveler.id))) }), _jsx("div", { className: "grid min-w-0 gap-4", children: seats.length === 0 ? (_jsx("div", { className: "rounded-md border border-dashed p-6 text-sm text-muted-foreground", children: messages.noSeats })) : (groups.map((group) => (_jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs(CardTitle, { className: "flex flex-wrap items-center gap-2 text-base", children: [_jsx(Armchair, { className: "size-4", "aria-hidden": "true" }), _jsx("span", { children: group.label }), _jsxs(Badge, { variant: "outline", children: [group.seats.filter((seat) => (occupants.byResource.get(seat.id) ?? []).length > 0).length, "/", group.seats.length] })] }) }), _jsx(CardContent, { children: _jsx("div", { className: "grid gap-2", children: seatRows(group.seats).map((row) => (_jsx("div", { className: "grid items-stretch gap-2", style: {
13
+ gridTemplateColumns: `repeat(${row.seats.length}, minmax(4.25rem, 1fr))`,
14
+ }, children: row.seats.map((seat) => {
15
+ const seatOccupants = occupants.byResource.get(seat.id) ?? [];
16
+ return (_jsx(VehicleSeatCell, { seat: seat, occupant: seatOccupants[0] ?? null, overflow: seatOccupants.length > 1, sharingGroupLabel: seatOccupants[0]?.sharingGroupId
17
+ ? sharingGroupLabels[seatOccupants[0].sharingGroupId]
18
+ : null, onDropTraveler: (travelerId) => onDropTraveler(travelerId, seat.id) }, seat.id));
19
+ }) }, row.rowKey))) }) })] }, group.id)))) })] }));
20
+ }
21
+ function VehicleSeatCell({ seat, occupant, overflow, sharingGroupLabel, onDropTraveler, }) {
22
+ const messages = useAllocationUiMessagesOrDefault();
23
+ const [over, setOver] = useState(false);
24
+ function onDrop(event) {
25
+ event.preventDefault();
26
+ setOver(false);
27
+ const travelerId = event.dataTransfer.getData("text/plain");
28
+ if (travelerId)
29
+ onDropTraveler(travelerId);
30
+ }
31
+ return (_jsxs("div", { id: `seat:${seat.id}`, className: cn("min-h-24 rounded-md border bg-background p-2 text-left text-xs transition-colors", over ? "border-primary bg-primary/5" /* i18n-literal-ok CSS class token */ : null, overflow
32
+ ? "border-destructive bg-destructive/5" /* i18n-literal-ok CSS class token */
33
+ : null), onDragOver: (event) => {
34
+ event.preventDefault();
35
+ setOver(true);
36
+ }, onDragLeave: () => setOver(false), onDrop: onDrop, children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("span", { className: "font-medium", children: seat.label ?? seatName(seat, messages) }), _jsx(SeatPositionBadge, { seat: seat })] }), occupant ? (_jsxs("div", { className: "mt-2 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1", children: [occupant.isLeadTraveler ? (_jsx(Crown, { className: "size-3 text-amber-500", "aria-label": messages.lead })) : null, _jsx("span", { className: "truncate font-medium", children: occupant.fullName })] }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-1 text-muted-foreground", children: [_jsx("span", { children: occupant.bookingNumber }), sharingGroupLabel ? (_jsx(Badge, { variant: "secondary", className: "max-w-full truncate text-[10px]", children: sharingGroupLabel })) : null] })] })) : (_jsx("div", { className: "mt-3 rounded border border-dashed p-2 text-muted-foreground", children: messages.dropHere }))] }));
37
+ }
@@ -0,0 +1,35 @@
1
+ import type { AllocationAuditLogEntry, AllocationManifestTraveler, AllocationResource } from "@voyantjs/availability-react";
2
+ import { type ReactNode } from "react";
3
+ import { type ValidationIssue } from "./slot-allocation-model.js";
4
+ export declare function DropColumn({ id, icon, title, description, count, capacity, disabled, action, children, onDropTraveler, }: {
5
+ id: string;
6
+ icon: ReactNode;
7
+ title: string;
8
+ description: string;
9
+ count: number;
10
+ capacity: number;
11
+ disabled?: boolean;
12
+ action?: ReactNode;
13
+ children: ReactNode;
14
+ onDropTraveler: (travelerId: string) => void;
15
+ }): import("react/jsx-runtime").JSX.Element;
16
+ export declare function TravelerTile({ traveler, sharingGroupLabel, renderActions, }: {
17
+ traveler: AllocationManifestTraveler;
18
+ sharingGroupLabel?: string | null;
19
+ renderActions?: (traveler: AllocationManifestTraveler) => ReactNode;
20
+ }): import("react/jsx-runtime").JSX.Element;
21
+ export declare function ValidationSummary({ issues, resources, unallocatedCount, }: {
22
+ issues: ValidationIssue[];
23
+ resources: AllocationResource[];
24
+ unallocatedCount: number;
25
+ }): import("react/jsx-runtime").JSX.Element;
26
+ export declare function ResourceFlagBadges({ resource }: {
27
+ resource: AllocationResource;
28
+ }): import("react/jsx-runtime").JSX.Element | null;
29
+ export declare function SeatPositionBadge({ seat }: {
30
+ seat: AllocationResource;
31
+ }): import("react/jsx-runtime").JSX.Element | null;
32
+ export declare function AuditLogCard({ entries }: {
33
+ entries: AllocationAuditLogEntry[];
34
+ }): import("react/jsx-runtime").JSX.Element | null;
35
+ //# sourceMappingURL=slot-allocation-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slot-allocation-shared.d.ts","sourceRoot":"","sources":["../../src/components/slot-allocation-shared.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EACnB,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EAAkB,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAGhE,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAE7E,wBAAgB,UAAU,CAAC,EACzB,EAAE,EACF,IAAI,EACJ,KAAK,EACL,WAAW,EACX,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,cAAc,GACf,EAAE;IACD,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;IACnB,cAAc,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7C,2CA6CA;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,iBAAiB,EACjB,aAAa,GACd,EAAE;IACD,QAAQ,EAAE,0BAA0B,CAAA;IACpC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,0BAA0B,KAAK,SAAS,CAAA;CACpE,2CA0CA;AAED,wBAAgB,iBAAiB,CAAC,EAChC,MAAM,EACN,SAAS,EACT,gBAAgB,GACjB,EAAE;IACD,MAAM,EAAE,eAAe,EAAE,CAAA;IACzB,SAAS,EAAE,kBAAkB,EAAE,CAAA;IAC/B,gBAAgB,EAAE,MAAM,CAAA;CACzB,2CA8BA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,kBAAkB,CAAA;CAAE,kDAoBhF;AAED,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,kDAmBvE;AAED,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,uBAAuB,EAAE,CAAA;CAAE,kDA4B/E"}