@voyantjs/availability-ui 0.30.7 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -7
- package/dist/components/availability-page.d.ts +50 -0
- package/dist/components/availability-page.d.ts.map +1 -0
- package/dist/components/availability-page.js +248 -0
- package/dist/components/availability-rule-detail-page.d.ts +240 -0
- package/dist/components/availability-rule-detail-page.d.ts.map +1 -0
- package/dist/components/availability-rule-detail-page.js +71 -0
- package/dist/components/availability-slot-detail-page.d.ts +583 -0
- package/dist/components/availability-slot-detail-page.d.ts.map +1 -0
- package/dist/components/availability-slot-detail-page.js +105 -0
- package/dist/components/availability-start-time-detail-page.d.ts +235 -0
- package/dist/components/availability-start-time-detail-page.d.ts.map +1 -0
- package/dist/components/availability-start-time-detail-page.js +80 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/provider.d.ts +1504 -0
- package/dist/i18n/provider.d.ts.map +1 -0
- package/dist/i18n/provider.js +88 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
# @voyantjs/availability-ui
|
|
2
2
|
|
|
3
|
-
Reusable availability UI primitives for Voyant
|
|
4
|
-
|
|
5
|
-
The initial package surface is intentionally leaf-level so downstream apps can
|
|
6
|
-
replace copied availability UI incrementally while page-level composition stays
|
|
7
|
-
app-owned.
|
|
3
|
+
Reusable availability UI primitives and page compositions for Voyant
|
|
4
|
+
operator/admin apps.
|
|
8
5
|
|
|
9
6
|
## Exports
|
|
10
7
|
|
|
11
8
|
| Entry | Description |
|
|
12
9
|
| --- | --- |
|
|
13
10
|
| `.` | Barrel re-exports |
|
|
11
|
+
| `./i18n` | Availability UI message provider, defaults, and helpers |
|
|
14
12
|
| `./components/*` | Availability UI components |
|
|
15
13
|
| `./utils` | Small formatting helpers |
|
|
16
14
|
|
|
17
15
|
## Surface
|
|
18
16
|
|
|
19
|
-
The package exports reusable pieces that keep app-owned
|
|
20
|
-
|
|
17
|
+
The package exports reusable pieces that keep app-owned routing and the
|
|
18
|
+
availability batch mutations injected through props:
|
|
21
19
|
|
|
20
|
+
- `AvailabilityPage`
|
|
21
|
+
- `AvailabilityRuleDetailPage`, `AvailabilitySlotDetailPage`,
|
|
22
|
+
`AvailabilityStartTimeDetailPage`
|
|
22
23
|
- `AvailabilityOverview`
|
|
23
24
|
- `AvailabilitySlotsTab`, `AvailabilityRulesTab`, `AvailabilityStartTimesTab`
|
|
24
25
|
- `AvailabilityCloseoutsTab`, `AvailabilityPickupPointsTab`
|
|
@@ -27,10 +28,79 @@ routing, and mutations injected through props:
|
|
|
27
28
|
- `AvailabilityPageSkeleton`, `AvailabilityBodySkeleton`, detail skeletons
|
|
28
29
|
- `availability*Columns` table column builders
|
|
29
30
|
- `AvailabilitySectionHeader`
|
|
31
|
+
- `AvailabilityUiMessagesProvider` and i18n helpers from `./i18n`
|
|
30
32
|
- `formatLocalizedSelectionLabel`
|
|
31
33
|
|
|
32
34
|
## Usage
|
|
33
35
|
|
|
36
|
+
`AvailabilityPage` owns the common operator availability shell, data hooks,
|
|
37
|
+
filters, overview metrics, table tabs, calendar tab, and rule/slot/start-time
|
|
38
|
+
dialogs. Route navigation and batch mutations stay app-specific:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { AvailabilityPage } from "@voyantjs/availability-ui"
|
|
42
|
+
|
|
43
|
+
<AvailabilityPage
|
|
44
|
+
onSlotOpen={(id) => navigate({ to: "/availability/$id", params: { id } })}
|
|
45
|
+
onRuleOpen={(id) => navigate({ to: "/availability/rules/$id", params: { id } })}
|
|
46
|
+
onStartTimeOpen={(id) =>
|
|
47
|
+
navigate({ to: "/availability/start-times/$id", params: { id } })
|
|
48
|
+
}
|
|
49
|
+
onProductOpen={(id) => navigate({ to: "/products/$id", params: { id } })}
|
|
50
|
+
onBulkUpdate={handleAvailabilityBulkUpdate}
|
|
51
|
+
onBulkDelete={handleAvailabilityBulkDelete}
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Closeout and pickup-point mutations are not owned by `availability-react` yet.
|
|
56
|
+
Pass `onCloseoutSubmit` and `onPickupPointSubmit` to use the package dialogs, or
|
|
57
|
+
use the `slots.dialogs` escape hatch to render app-owned dialogs.
|
|
58
|
+
|
|
59
|
+
Detail pages expose query/loader helpers that accept the app's API client:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import {
|
|
63
|
+
AvailabilitySlotDetailPage,
|
|
64
|
+
loadAvailabilitySlotDetailPage,
|
|
65
|
+
} from "@voyantjs/availability-ui"
|
|
66
|
+
import { defaultFetcher } from "@voyantjs/availability-react"
|
|
67
|
+
|
|
68
|
+
const client = { baseUrl: getApiUrl(), fetcher: defaultFetcher }
|
|
69
|
+
|
|
70
|
+
export const Route = createFileRoute("/_workspace/availability/$id")({
|
|
71
|
+
loader: ({ context, params }) =>
|
|
72
|
+
loadAvailabilitySlotDetailPage(context.queryClient, client, params.id),
|
|
73
|
+
component: () => {
|
|
74
|
+
const { id } = Route.useParams()
|
|
75
|
+
return (
|
|
76
|
+
<AvailabilitySlotDetailPage
|
|
77
|
+
id={id}
|
|
78
|
+
onBack={() => navigate({ to: "/availability" })}
|
|
79
|
+
onOpenProduct={(productId) =>
|
|
80
|
+
navigate({ to: "/products/$id", params: { id: productId } })
|
|
81
|
+
}
|
|
82
|
+
onOpenStartTime={(startTimeId) =>
|
|
83
|
+
navigate({ to: "/availability/start-times/$id", params: { id: startTimeId } })
|
|
84
|
+
}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Wrap consumers in `AvailabilityUiMessagesProvider` for package-level copy and
|
|
92
|
+
locale-aware formatting. Without a provider the package falls back to English.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { AvailabilityUiMessagesProvider } from "@voyantjs/availability-ui/i18n"
|
|
96
|
+
|
|
97
|
+
<AvailabilityUiMessagesProvider locale={resolvedLocale}>
|
|
98
|
+
<AvailabilityPage onBulkUpdate={handleBulkUpdate} onBulkDelete={handleBulkDelete} />
|
|
99
|
+
</AvailabilityUiMessagesProvider>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Leaf components remain available for custom page shells:
|
|
103
|
+
|
|
34
104
|
```tsx
|
|
35
105
|
import { AvailabilitySectionHeader } from "@voyantjs/availability-ui"
|
|
36
106
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type AvailabilityCloseoutRow, type AvailabilityPickupPointRow, type AvailabilitySlotRow } from "@voyantjs/availability-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { type AvailabilityCloseoutSubmitPayload, type AvailabilityPickupPointSubmitPayload, type AvailabilityRuleSubmitPayload, type AvailabilitySlotSubmitPayload, type AvailabilityStartTimeSubmitPayload } from "./availability-dialogs.js";
|
|
4
|
+
import { type AvailabilityBulkDeleteFn, type AvailabilityBulkUpdateFn } from "./availability-tabs.js";
|
|
5
|
+
export type AvailabilityPageTab = "slots" | "rules" | "start-times" | "closeouts" | "pickup-points" | "calendar";
|
|
6
|
+
export type AvailabilityPageActiveFilter = "all" | "active" | "inactive";
|
|
7
|
+
export type AvailabilityPageSlotStatusFilter = "all" | AvailabilitySlotRow["status"];
|
|
8
|
+
export type AvailabilityPageBulkUpdateHandler = AvailabilityBulkUpdateFn;
|
|
9
|
+
export type AvailabilityPageBulkDeleteHandler = AvailabilityBulkDeleteFn;
|
|
10
|
+
type DialogSubmitContext = {
|
|
11
|
+
isEditing: boolean;
|
|
12
|
+
id?: string;
|
|
13
|
+
};
|
|
14
|
+
export type AvailabilityPageRuleSubmitHandler = (payload: AvailabilityRuleSubmitPayload, context: DialogSubmitContext) => Promise<void>;
|
|
15
|
+
export type AvailabilityPageStartTimeSubmitHandler = (payload: AvailabilityStartTimeSubmitPayload, context: DialogSubmitContext) => Promise<void>;
|
|
16
|
+
export type AvailabilityPageSlotSubmitHandler = (payload: AvailabilitySlotSubmitPayload, context: DialogSubmitContext) => Promise<void>;
|
|
17
|
+
export type AvailabilityPageCloseoutSubmitHandler = (payload: AvailabilityCloseoutSubmitPayload, context: DialogSubmitContext) => Promise<void>;
|
|
18
|
+
export type AvailabilityPagePickupPointSubmitHandler = (payload: AvailabilityPickupPointSubmitPayload, context: DialogSubmitContext) => Promise<void>;
|
|
19
|
+
export interface AvailabilityPageSlots {
|
|
20
|
+
headerEnd?: ReactNode;
|
|
21
|
+
beforeOverview?: ReactNode;
|
|
22
|
+
afterOverview?: ReactNode;
|
|
23
|
+
beforeTabs?: ReactNode;
|
|
24
|
+
afterTabs?: ReactNode;
|
|
25
|
+
dialogs?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export interface AvailabilityPageProps {
|
|
28
|
+
className?: string;
|
|
29
|
+
defaultTab?: AvailabilityPageTab;
|
|
30
|
+
bulkActionTarget?: string | null;
|
|
31
|
+
onBulkUpdate: AvailabilityPageBulkUpdateHandler;
|
|
32
|
+
onBulkDelete: AvailabilityPageBulkDeleteHandler;
|
|
33
|
+
onSlotOpen?: (slotId: string) => void;
|
|
34
|
+
onRuleOpen?: (ruleId: string) => void;
|
|
35
|
+
onStartTimeOpen?: (startTimeId: string) => void;
|
|
36
|
+
onProductOpen?: (productId: string) => void;
|
|
37
|
+
onCloseoutCreate?: () => void;
|
|
38
|
+
onCloseoutEdit?: (closeout: AvailabilityCloseoutRow) => void;
|
|
39
|
+
onPickupPointCreate?: () => void;
|
|
40
|
+
onPickupPointEdit?: (pickupPoint: AvailabilityPickupPointRow) => void;
|
|
41
|
+
onRuleSubmit?: AvailabilityPageRuleSubmitHandler;
|
|
42
|
+
onStartTimeSubmit?: AvailabilityPageStartTimeSubmitHandler;
|
|
43
|
+
onSlotSubmit?: AvailabilityPageSlotSubmitHandler;
|
|
44
|
+
onCloseoutSubmit?: AvailabilityPageCloseoutSubmitHandler;
|
|
45
|
+
onPickupPointSubmit?: AvailabilityPagePickupPointSubmitHandler;
|
|
46
|
+
slots?: AvailabilityPageSlots;
|
|
47
|
+
}
|
|
48
|
+
export declare function AvailabilityPage({ className, defaultTab, bulkActionTarget, onBulkUpdate, onBulkDelete, onSlotOpen, onRuleOpen, onStartTimeOpen, onProductOpen, onCloseoutCreate, onCloseoutEdit, onPickupPointCreate, onPickupPointEdit, onRuleSubmit, onStartTimeSubmit, onSlotSubmit, onCloseoutSubmit, onPickupPointSubmit, slots: pageSlots, }: AvailabilityPageProps): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
export {};
|
|
50
|
+
//# sourceMappingURL=availability-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"availability-page.d.ts","sourceRoot":"","sources":["../../src/components/availability-page.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { availabilityQueryKeys, useAvailabilityRuleMutation, useAvailabilitySlotMutation, useAvailabilityStartTimeMutation, useCloseouts, usePickupPoints, useProducts, useRules, useSlots, useStartTimes, } from "@voyantjs/availability-react";
|
|
5
|
+
import { Button, cn, Label } from "@voyantjs/ui/components";
|
|
6
|
+
import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
|
|
7
|
+
import { CalendarProvider, CalendarView, } from "@voyantjs/ui/components/big-calendar";
|
|
8
|
+
import { DateRangePicker } from "@voyantjs/ui/components/date-picker";
|
|
9
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components/select";
|
|
10
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@voyantjs/ui/components/tabs";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { useAvailabilityUiMessagesOrDefault } from "../i18n/index.js";
|
|
13
|
+
import { AvailabilityCloseoutDialog, AvailabilityPickupPointDialog, AvailabilityRuleDialog, AvailabilitySlotDialog, AvailabilityStartTimeDialog, } from "./availability-dialogs.js";
|
|
14
|
+
import { AvailabilityOverview } from "./availability-overview.js";
|
|
15
|
+
import { AvailabilityBodySkeleton } from "./availability-skeletons.js";
|
|
16
|
+
import { AvailabilityCloseoutsTab, AvailabilityPickupPointsTab, AvailabilityRulesTab, AvailabilitySlotsTab, AvailabilityStartTimesTab, } from "./availability-tabs.js";
|
|
17
|
+
const noop = () => undefined;
|
|
18
|
+
const noopId = (_id) => undefined;
|
|
19
|
+
const noopCloseout = (_row) => undefined;
|
|
20
|
+
const noopPickupPoint = (_row) => undefined;
|
|
21
|
+
export function AvailabilityPage({ className, defaultTab = "slots", bulkActionTarget = null, onBulkUpdate, onBulkDelete, onSlotOpen = noopId, onRuleOpen = noopId, onStartTimeOpen = noopId, onProductOpen = noopId, onCloseoutCreate = noop, onCloseoutEdit = noopCloseout, onPickupPointCreate = noop, onPickupPointEdit = noopPickupPoint, onRuleSubmit, onStartTimeSubmit, onSlotSubmit, onCloseoutSubmit, onPickupPointSubmit, slots: pageSlots, }) {
|
|
22
|
+
const messages = useAvailabilityUiMessagesOrDefault();
|
|
23
|
+
const page = messages.page;
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
const ruleMutation = useAvailabilityRuleMutation();
|
|
26
|
+
const startTimeMutation = useAvailabilityStartTimeMutation();
|
|
27
|
+
const slotMutation = useAvailabilitySlotMutation();
|
|
28
|
+
const [productFilter, setProductFilter] = useState("all");
|
|
29
|
+
const [productSearch, setProductSearch] = useState("");
|
|
30
|
+
const [slotStatusFilter, setSlotStatusFilter] = useState("all");
|
|
31
|
+
const [slotDateRange, setSlotDateRange] = useState(null);
|
|
32
|
+
const [ruleActiveFilter, setRuleActiveFilter] = useState("all");
|
|
33
|
+
const [startTimeActiveFilter, setStartTimeActiveFilter] = useState("all");
|
|
34
|
+
const [closeoutDateRange, setCloseoutDateRange] = useState(null);
|
|
35
|
+
const [pickupPointActiveFilter, setPickupPointActiveFilter] = useState("all");
|
|
36
|
+
const [activeTab, setActiveTab] = useState(defaultTab);
|
|
37
|
+
const [calendarView, setCalendarView] = useState("month");
|
|
38
|
+
const [ruleSelection, setRuleSelection] = useState({});
|
|
39
|
+
const [startTimeSelection, setStartTimeSelection] = useState({});
|
|
40
|
+
const [slotSelection, setSlotSelection] = useState({});
|
|
41
|
+
const [closeoutSelection, setCloseoutSelection] = useState({});
|
|
42
|
+
const [pickupPointSelection, setPickupPointSelection] = useState({});
|
|
43
|
+
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
|
44
|
+
const [startTimeDialogOpen, setStartTimeDialogOpen] = useState(false);
|
|
45
|
+
const [slotDialogOpen, setSlotDialogOpen] = useState(false);
|
|
46
|
+
const [closeoutDialogOpen, setCloseoutDialogOpen] = useState(false);
|
|
47
|
+
const [pickupPointDialogOpen, setPickupPointDialogOpen] = useState(false);
|
|
48
|
+
const [editingRule, setEditingRule] = useState();
|
|
49
|
+
const [editingStartTime, setEditingStartTime] = useState();
|
|
50
|
+
const [editingSlot, setEditingSlot] = useState();
|
|
51
|
+
const [editingCloseout, setEditingCloseout] = useState();
|
|
52
|
+
const [editingPickupPoint, setEditingPickupPoint] = useState();
|
|
53
|
+
const productsQuery = useProducts({ search: productSearch || undefined, limit: 25, offset: 0 });
|
|
54
|
+
const rulesQuery = useRules({ limit: 25, offset: 0 });
|
|
55
|
+
const startTimesQuery = useStartTimes({ limit: 25, offset: 0 });
|
|
56
|
+
const slotsQuery = useSlots({ limit: 25, offset: 0 });
|
|
57
|
+
const closeoutsQuery = useCloseouts({ limit: 25, offset: 0 });
|
|
58
|
+
const pickupPointsQuery = usePickupPoints({ limit: 25, offset: 0 });
|
|
59
|
+
const products = productsQuery.data?.data ?? [];
|
|
60
|
+
const rules = rulesQuery.data?.data ?? [];
|
|
61
|
+
const startTimes = startTimesQuery.data?.data ?? [];
|
|
62
|
+
const availabilitySlots = slotsQuery.data?.data ?? [];
|
|
63
|
+
const closeouts = closeoutsQuery.data?.data ?? [];
|
|
64
|
+
const pickupPoints = pickupPointsQuery.data?.data ?? [];
|
|
65
|
+
const matchesProduct = (productId) => productFilter === "all" || productId === productFilter;
|
|
66
|
+
const matchesActive = (active, filter) => filter === "all" || (filter === "active" ? active : !active);
|
|
67
|
+
const matchesDateRange = (date, range) => (!range?.from || date >= range.from) && (!range?.to || date <= range.to);
|
|
68
|
+
const filteredRules = rules.filter((rule) => matchesProduct(rule.productId) && matchesActive(rule.active, ruleActiveFilter));
|
|
69
|
+
const filteredStartTimes = startTimes.filter((startTime) => matchesProduct(startTime.productId) && matchesActive(startTime.active, startTimeActiveFilter));
|
|
70
|
+
const productFilteredSlots = availabilitySlots.filter((slot) => matchesProduct(slot.productId));
|
|
71
|
+
const filteredSlots = productFilteredSlots.filter((slot) => (slotStatusFilter === "all" || slot.status === slotStatusFilter) &&
|
|
72
|
+
matchesDateRange(slot.dateLocal, slotDateRange));
|
|
73
|
+
const filteredCloseouts = closeouts.filter((closeout) => matchesProduct(closeout.productId) && matchesDateRange(closeout.dateLocal, closeoutDateRange));
|
|
74
|
+
const filteredPickupPoints = pickupPoints.filter((pickupPoint) => matchesProduct(pickupPoint.productId) &&
|
|
75
|
+
matchesActive(pickupPoint.active, pickupPointActiveFilter));
|
|
76
|
+
const filteredProducts = products.filter((product) => productFilter === "all" || product.id === productFilter);
|
|
77
|
+
const constrainedSlots = [...filteredSlots]
|
|
78
|
+
.filter((slot) => slot.status === "sold_out" || slot.status === "closed")
|
|
79
|
+
.sort((left, right) => left.startsAt.localeCompare(right.startsAt));
|
|
80
|
+
const nowIso = new Date().toISOString();
|
|
81
|
+
const productsWithoutUpcomingDepartures = filteredProducts.filter((product) => !productFilteredSlots.some((slot) => slot.productId === product.id && slot.status === "open" && slot.startsAt >= nowIso));
|
|
82
|
+
const hasFilters = productFilter !== "all";
|
|
83
|
+
const selectedProduct = products.find((product) => product.id === productFilter) ?? null;
|
|
84
|
+
const slotStatusToColor = {
|
|
85
|
+
open: "green",
|
|
86
|
+
closed: "gray",
|
|
87
|
+
sold_out: "red",
|
|
88
|
+
cancelled: "yellow",
|
|
89
|
+
};
|
|
90
|
+
const calendarEvents = filteredSlots.map((slot) => {
|
|
91
|
+
const productName = products.find((product) => product.id === slot.productId)?.name;
|
|
92
|
+
return {
|
|
93
|
+
id: slot.id,
|
|
94
|
+
startDate: slot.startsAt,
|
|
95
|
+
endDate: slot.endsAt ?? slot.startsAt,
|
|
96
|
+
title: productName ?? slot.productName ?? messages.tabs.slots.title,
|
|
97
|
+
description: slot.notes ?? "",
|
|
98
|
+
color: slotStatusToColor[slot.status],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
const queries = [
|
|
102
|
+
productsQuery,
|
|
103
|
+
rulesQuery,
|
|
104
|
+
startTimesQuery,
|
|
105
|
+
slotsQuery,
|
|
106
|
+
closeoutsQuery,
|
|
107
|
+
pickupPointsQuery,
|
|
108
|
+
];
|
|
109
|
+
const isLoading = queries.some((query) => query.isPending);
|
|
110
|
+
const isError = queries.some((query) => query.isError);
|
|
111
|
+
const refreshAll = async () => {
|
|
112
|
+
await queryClient.invalidateQueries({ queryKey: availabilityQueryKeys.all });
|
|
113
|
+
};
|
|
114
|
+
const handleRuleSubmit = onRuleSubmit ??
|
|
115
|
+
(async (payload, context) => {
|
|
116
|
+
if (context.isEditing) {
|
|
117
|
+
await ruleMutation.update.mutateAsync({
|
|
118
|
+
id: requireEditingId(context),
|
|
119
|
+
input: payload,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await ruleMutation.create.mutateAsync(payload);
|
|
124
|
+
});
|
|
125
|
+
const handleStartTimeSubmit = onStartTimeSubmit ??
|
|
126
|
+
(async (payload, context) => {
|
|
127
|
+
if (context.isEditing) {
|
|
128
|
+
await startTimeMutation.update.mutateAsync({
|
|
129
|
+
id: requireEditingId(context),
|
|
130
|
+
input: payload,
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
await startTimeMutation.create.mutateAsync(payload);
|
|
135
|
+
});
|
|
136
|
+
const handleSlotSubmit = onSlotSubmit ??
|
|
137
|
+
(async (payload, context) => {
|
|
138
|
+
if (context.isEditing) {
|
|
139
|
+
await slotMutation.update.mutateAsync({
|
|
140
|
+
id: requireEditingId(context),
|
|
141
|
+
input: payload,
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await slotMutation.create.mutateAsync(payload);
|
|
146
|
+
});
|
|
147
|
+
const closeRuleDialog = () => {
|
|
148
|
+
setRuleDialogOpen(false);
|
|
149
|
+
setEditingRule(undefined);
|
|
150
|
+
};
|
|
151
|
+
const closeStartTimeDialog = () => {
|
|
152
|
+
setStartTimeDialogOpen(false);
|
|
153
|
+
setEditingStartTime(undefined);
|
|
154
|
+
};
|
|
155
|
+
const closeSlotDialog = () => {
|
|
156
|
+
setSlotDialogOpen(false);
|
|
157
|
+
setEditingSlot(undefined);
|
|
158
|
+
};
|
|
159
|
+
const closeCloseoutDialog = () => {
|
|
160
|
+
setCloseoutDialogOpen(false);
|
|
161
|
+
setEditingCloseout(undefined);
|
|
162
|
+
};
|
|
163
|
+
const closePickupPointDialog = () => {
|
|
164
|
+
setPickupPointDialogOpen(false);
|
|
165
|
+
setEditingPickupPoint(undefined);
|
|
166
|
+
};
|
|
167
|
+
return (_jsxs("div", { className: cn("flex flex-col gap-6 p-6", className), children: [_jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-start md:justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: messages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: messages.description })] }), _jsxs("div", { className: "flex w-full flex-col gap-3 md:w-72", children: [_jsx(AsyncCombobox, { value: productFilter === "all" ? null : productFilter, onChange: (value) => setProductFilter(value ?? "all"), items: products, selectedItem: selectedProduct, getKey: (product) => product.id, getLabel: (product) => product.name, onSearchChange: setProductSearch, placeholder: messages.allProducts, emptyText: productsQuery.isFetching ? page.loading : page.filters.productSearchEmpty, triggerClassName: "w-full" }), pageSlots?.headerEnd] })] }), isLoading ? (_jsx(AvailabilityBodySkeleton, {})) : isError ? (_jsx("div", { className: "rounded-md border p-6 text-sm text-muted-foreground", children: page.loadFailed })) : (_jsxs(_Fragment, { children: [pageSlots?.beforeOverview, _jsx(AvailabilityOverview, { messages: messages, products: products, constrainedSlots: constrainedSlots, openSlotsCount: filteredSlots.filter((slot) => slot.status === "open").length, filteredRules: filteredRules, filteredPickupPoints: filteredPickupPoints, productsWithoutUpcomingDepartures: productsWithoutUpcomingDepartures, search: "", setSearch: () => { }, productFilter: productFilter, setProductFilter: setProductFilter, hasFilters: hasFilters, onClearFilters: () => setProductFilter("all"), onOpenSlot: onSlotOpen, onOpenProduct: onProductOpen, onJumpToSlots: () => setActiveTab("slots"), showFilters: false }), pageSlots?.afterOverview, pageSlots?.beforeTabs, _jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab((value ?? "slots")), children: [_jsxs(TabsList, { className: "flex w-full justify-start overflow-x-auto", children: [_jsx(TabsTrigger, { value: "slots", children: messages.tabSlots }), _jsx(TabsTrigger, { value: "rules", children: messages.tabRules }), _jsx(TabsTrigger, { value: "start-times", children: messages.tabStartTimes }), _jsx(TabsTrigger, { value: "closeouts", children: messages.tabCloseouts }), _jsx(TabsTrigger, { value: "pickup-points", children: messages.tabPickupPoints }), _jsx(TabsTrigger, { value: "calendar", children: page.calendarTab })] }), _jsx(AvailabilitySlotsTab, { messages: messages, products: products, filteredSlots: filteredSlots, slotSelection: slotSelection, setSlotSelection: setSlotSelection, bulkActionTarget: bulkActionTarget, handleBulkUpdate: onBulkUpdate, handleBulkDelete: onBulkDelete, onCreate: () => {
|
|
168
|
+
setEditingSlot(undefined);
|
|
169
|
+
setSlotDialogOpen(true);
|
|
170
|
+
}, onOpenRoute: onSlotOpen, onEdit: (row) => {
|
|
171
|
+
setEditingSlot(row);
|
|
172
|
+
setSlotDialogOpen(true);
|
|
173
|
+
}, toolbar: _jsx(SlotsToolbar, { value: slotStatusFilter, onValueChange: setSlotStatusFilter, dateRange: slotDateRange, onDateRangeChange: setSlotDateRange }) }), _jsx(AvailabilityRulesTab, { messages: messages, products: products, filteredRules: filteredRules, ruleSelection: ruleSelection, setRuleSelection: setRuleSelection, bulkActionTarget: bulkActionTarget, handleBulkUpdate: onBulkUpdate, handleBulkDelete: onBulkDelete, onCreate: () => {
|
|
174
|
+
setEditingRule(undefined);
|
|
175
|
+
setRuleDialogOpen(true);
|
|
176
|
+
}, onOpenRoute: onRuleOpen, onEdit: (row) => {
|
|
177
|
+
setEditingRule(row);
|
|
178
|
+
setRuleDialogOpen(true);
|
|
179
|
+
}, toolbar: _jsx(ActiveToolbar, { value: ruleActiveFilter, onValueChange: setRuleActiveFilter }) }), _jsx(AvailabilityStartTimesTab, { messages: messages, products: products, filteredStartTimes: filteredStartTimes, startTimeSelection: startTimeSelection, setStartTimeSelection: setStartTimeSelection, bulkActionTarget: bulkActionTarget, handleBulkUpdate: onBulkUpdate, handleBulkDelete: onBulkDelete, onCreate: () => {
|
|
180
|
+
setEditingStartTime(undefined);
|
|
181
|
+
setStartTimeDialogOpen(true);
|
|
182
|
+
}, onOpenRoute: onStartTimeOpen, onEdit: (row) => {
|
|
183
|
+
setEditingStartTime(row);
|
|
184
|
+
setStartTimeDialogOpen(true);
|
|
185
|
+
}, toolbar: _jsx(ActiveToolbar, { value: startTimeActiveFilter, onValueChange: setStartTimeActiveFilter }) }), _jsx(AvailabilityCloseoutsTab, { messages: messages, products: products, filteredCloseouts: filteredCloseouts, closeoutSelection: closeoutSelection, setCloseoutSelection: setCloseoutSelection, bulkActionTarget: bulkActionTarget, handleBulkDelete: onBulkDelete, onCreate: onCloseoutSubmit
|
|
186
|
+
? () => {
|
|
187
|
+
setEditingCloseout(undefined);
|
|
188
|
+
setCloseoutDialogOpen(true);
|
|
189
|
+
}
|
|
190
|
+
: onCloseoutCreate, onEdit: (row) => {
|
|
191
|
+
if (onCloseoutSubmit) {
|
|
192
|
+
setEditingCloseout(row);
|
|
193
|
+
setCloseoutDialogOpen(true);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
onCloseoutEdit(row);
|
|
197
|
+
}, toolbar: _jsx(DateRangeToolbar, { value: closeoutDateRange, onValueChange: setCloseoutDateRange }) }), _jsx(AvailabilityPickupPointsTab, { messages: messages, products: products, filteredPickupPoints: filteredPickupPoints, pickupPointSelection: pickupPointSelection, setPickupPointSelection: setPickupPointSelection, bulkActionTarget: bulkActionTarget, handleBulkUpdate: onBulkUpdate, handleBulkDelete: onBulkDelete, onCreate: onPickupPointSubmit
|
|
198
|
+
? () => {
|
|
199
|
+
setEditingPickupPoint(undefined);
|
|
200
|
+
setPickupPointDialogOpen(true);
|
|
201
|
+
}
|
|
202
|
+
: onPickupPointCreate, onEdit: (row) => {
|
|
203
|
+
if (onPickupPointSubmit) {
|
|
204
|
+
setEditingPickupPoint(row);
|
|
205
|
+
setPickupPointDialogOpen(true);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
onPickupPointEdit(row);
|
|
209
|
+
}, toolbar: _jsx(ActiveToolbar, { value: pickupPointActiveFilter, onValueChange: setPickupPointActiveFilter }) }), _jsx(TabsContent, { value: "calendar", className: "flex flex-col gap-4", children: _jsx(CalendarProvider, { events: calendarEvents, onEventClick: (event) => onSlotOpen(event.id), children: _jsx(CalendarView, { view: calendarView, onViewChange: setCalendarView, onDayClick: () => setCalendarView("day") }) }) })] }), pageSlots?.afterTabs] })), _jsx(AvailabilityRuleDialog, { messages: messages, open: ruleDialogOpen, onOpenChange: setRuleDialogOpen, rule: editingRule, products: products, onSubmit: handleRuleSubmit, onSuccess: () => {
|
|
210
|
+
closeRuleDialog();
|
|
211
|
+
void refreshAll();
|
|
212
|
+
} }), _jsx(AvailabilityStartTimeDialog, { messages: messages, open: startTimeDialogOpen, onOpenChange: setStartTimeDialogOpen, startTime: editingStartTime, products: products, onSubmit: handleStartTimeSubmit, onSuccess: () => {
|
|
213
|
+
closeStartTimeDialog();
|
|
214
|
+
void refreshAll();
|
|
215
|
+
} }), _jsx(AvailabilitySlotDialog, { messages: messages, open: slotDialogOpen, onOpenChange: setSlotDialogOpen, slot: editingSlot, products: products, rules: rules, startTimes: startTimes, onSubmit: handleSlotSubmit, onSuccess: () => {
|
|
216
|
+
closeSlotDialog();
|
|
217
|
+
void refreshAll();
|
|
218
|
+
} }), onCloseoutSubmit ? (_jsx(AvailabilityCloseoutDialog, { messages: messages, open: closeoutDialogOpen, onOpenChange: setCloseoutDialogOpen, closeout: editingCloseout, products: products, slots: availabilitySlots, onSubmit: onCloseoutSubmit, onSuccess: () => {
|
|
219
|
+
closeCloseoutDialog();
|
|
220
|
+
void refreshAll();
|
|
221
|
+
} })) : null, onPickupPointSubmit ? (_jsx(AvailabilityPickupPointDialog, { messages: messages, open: pickupPointDialogOpen, onOpenChange: setPickupPointDialogOpen, pickupPoint: editingPickupPoint, products: products, onSubmit: onPickupPointSubmit, onSuccess: () => {
|
|
222
|
+
closePickupPointDialog();
|
|
223
|
+
void refreshAll();
|
|
224
|
+
} })) : null, pageSlots?.dialogs] }));
|
|
225
|
+
}
|
|
226
|
+
function requireEditingId(context) {
|
|
227
|
+
if (!context.id)
|
|
228
|
+
throw new Error("AvailabilityPage edit submit requires an id.");
|
|
229
|
+
return context.id;
|
|
230
|
+
}
|
|
231
|
+
function SlotsToolbar({ value, onValueChange, dateRange, onDateRangeChange, }) {
|
|
232
|
+
const messages = useAvailabilityUiMessagesOrDefault();
|
|
233
|
+
const page = messages.page;
|
|
234
|
+
const hasFilters = value !== "all" || Boolean(dateRange?.from) || Boolean(dateRange?.to);
|
|
235
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "availability-slot-status", className: "text-xs", children: page.filters.statusLabel }), _jsxs(Select, { value: value, onValueChange: (nextValue) => onValueChange((nextValue ?? "all")), children: [_jsx(SelectTrigger, { id: "availability-slot-status", className: "w-full sm:w-44", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: _jsxs(SelectGroup, { children: [_jsx(SelectItem, { value: "all", children: page.filters.allStatuses }), _jsx(SelectItem, { value: "open", children: messages.statusOpen }), _jsx(SelectItem, { value: "closed", children: messages.statusClosed }), _jsx(SelectItem, { value: "sold_out", children: messages.statusSoldOut }), _jsx(SelectItem, { value: "cancelled", children: messages.statusCancelled })] }) })] })] }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: page.filters.dateRangeLabel }), _jsx(DateRangePicker, { value: dateRange, onChange: onDateRangeChange, className: "w-full sm:w-72", placeholder: page.filters.anyDate })] }), hasFilters ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => {
|
|
236
|
+
onValueChange("all");
|
|
237
|
+
onDateRangeChange(null);
|
|
238
|
+
}, children: page.filters.reset })) : null] }));
|
|
239
|
+
}
|
|
240
|
+
function DateRangeToolbar({ value, onValueChange, }) {
|
|
241
|
+
const page = useAvailabilityUiMessagesOrDefault().page;
|
|
242
|
+
const hasFilters = Boolean(value?.from) || Boolean(value?.to);
|
|
243
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { className: "text-xs", children: page.filters.dateRangeLabel }), _jsx(DateRangePicker, { value: value, onChange: onValueChange, className: "w-full sm:w-72", placeholder: page.filters.anyDate })] }), hasFilters ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => onValueChange(null), children: page.filters.reset })) : null] }));
|
|
244
|
+
}
|
|
245
|
+
function ActiveToolbar({ value, onValueChange, }) {
|
|
246
|
+
const page = useAvailabilityUiMessagesOrDefault().page;
|
|
247
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: "availability-active-filter", className: "text-xs", children: page.filters.stateLabel }), _jsxs(Select, { value: value, onValueChange: (nextValue) => onValueChange((nextValue ?? "all")), children: [_jsx(SelectTrigger, { id: "availability-active-filter", className: "w-full sm:w-44", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: _jsxs(SelectGroup, { children: [_jsx(SelectItem, { value: "all", children: page.filters.allStates }), _jsx(SelectItem, { value: "active", children: page.filters.active }), _jsx(SelectItem, { value: "inactive", children: page.filters.inactive })] }) })] })] }), value !== "all" ? (_jsx(Button, { variant: "outline", size: "sm", onClick: () => onValueChange("all"), children: page.filters.reset })) : null] }));
|
|
248
|
+
}
|