@fayz-ai/plugin-menu 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/MenuContext.d.ts +30 -0
  4. package/dist/MenuContext.d.ts.map +1 -0
  5. package/dist/MenuPage-OABV6JYB.js +520 -0
  6. package/dist/MenuPage-OABV6JYB.js.map +1 -0
  7. package/dist/MenuPage-VMI2YAPY.cjs +522 -0
  8. package/dist/MenuPage-VMI2YAPY.cjs.map +1 -0
  9. package/dist/MenuPage.d.ts +13 -0
  10. package/dist/MenuPage.d.ts.map +1 -0
  11. package/dist/data/fayz.d.ts +12 -0
  12. package/dist/data/fayz.d.ts.map +1 -0
  13. package/dist/data/mock.d.ts +3 -0
  14. package/dist/data/mock.d.ts.map +1 -0
  15. package/dist/data/types.d.ts +22 -0
  16. package/dist/data/types.d.ts.map +1 -0
  17. package/dist/index.cjs +1053 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.ts +34 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +1046 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/locales/en.d.ts +2 -0
  24. package/dist/locales/en.d.ts.map +1 -0
  25. package/dist/locales/index.d.ts +2 -0
  26. package/dist/locales/index.d.ts.map +1 -0
  27. package/dist/locales/pt-BR.d.ts +2 -0
  28. package/dist/locales/pt-BR.d.ts.map +1 -0
  29. package/dist/registries.d.ts +3 -0
  30. package/dist/registries.d.ts.map +1 -0
  31. package/dist/store.d.ts +26 -0
  32. package/dist/store.d.ts.map +1 -0
  33. package/dist/types.d.ts +112 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/views/MenuManagerView.d.ts +3 -0
  36. package/dist/views/MenuManagerView.d.ts.map +1 -0
  37. package/package.json +55 -0
  38. package/src/MenuContext.tsx +40 -0
  39. package/src/MenuPage.tsx +21 -0
  40. package/src/data/fayz.ts +429 -0
  41. package/src/data/mock.ts +289 -0
  42. package/src/data/types.ts +33 -0
  43. package/src/index.ts +184 -0
  44. package/src/locales/en.ts +64 -0
  45. package/src/locales/index.ts +4 -0
  46. package/src/locales/pt-BR.ts +64 -0
  47. package/src/registries.ts +84 -0
  48. package/src/store.ts +161 -0
  49. package/src/types.ts +145 -0
  50. package/src/views/MenuManagerView.tsx +688 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Faya Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @fayz-ai/plugin-menu
2
+
3
+ > The menu engine behind every food vertical — items, categories, modifiers, 86'd in one tap.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@fayz-ai/plugin-menu.svg)](https://www.npmjs.com/package/@fayz-ai/plugin-menu)
6
+ [![license](https://img.shields.io/npm/l/@fayz-ai/plugin-menu.svg)](https://github.com/FayaLabs/fayz-sdk/blob/main/LICENSE)
7
+
8
+ A menu is the product catalog of a food business — and it changes by the hour. `plugin-menu` is the management surface for that: items, categories, allergens, and modifiers, with sold-out toggles that take effect instantly. It's the foundation the orders flow reads from, so the two compose into a working restaurant.
9
+
10
+ It's provider-first by design. Ship a demo on the bundled mock provider, then drop in `createFayzMenuProvider` (or your own `MenuDataProvider`) for real data. Modifiers and delivery pricing are opt-in modules, labels and currency are configurable, and the host AI can list items and flip availability by name. The intent: menu management shouldn't be a bespoke build — it's a plugin you snap into a `defineSaas` app to compose a real food SaaS.
11
+
12
+ ## What's inside
13
+ - **Navigation + page** — a `/menu` surface with a menu manager (`MenuManagerView`) for items and categories
14
+ - **Pluggable data** — inject a `MenuDataProvider`, use `createFayzMenuProvider`, or fall back to the bundled mock provider
15
+ - **Opt-in modules** — `modifiers` (on by default) and `deliveryPricing` (off by default)
16
+ - **AI tools** — `listMenuItems` (filter by category/status) and `toggleMenuItemAvailability` (mark available or sold out by name)
17
+ - **Settings + i18n** — a settings panel for categories, allergens, and modifiers, plus bundled locales
18
+ - **Public domain types** — exported so any host can implement its own `MenuDataProvider`
19
+
20
+ ## Install
21
+ ```bash
22
+ npm install @fayz-ai/plugin-menu
23
+ ```
24
+ Peer deps: `@fayz-ai/core`, `@fayz-ai/ui`, `@fayz-ai/saas`, `@fayz-ai/sdk` (+ react, react-dom).
25
+
26
+ ## Usage
27
+ ```tsx
28
+ import { defineSaas } from '@fayz-ai/saas'
29
+ import { createMenuPlugin, createFayzMenuProvider } from '@fayz-ai/plugin-menu'
30
+
31
+ export const app = defineSaas({
32
+ // ...
33
+ plugins: [
34
+ createMenuPlugin({
35
+ modules: { modifiers: true, deliveryPricing: false },
36
+ currency: { code: 'BRL', locale: 'pt-BR', symbol: 'R$' },
37
+ dataProvider: createFayzMenuProvider({ /* ... */ }),
38
+ }),
39
+ ],
40
+ })
41
+ ```
42
+
43
+ Options: `modules`, `labels`, `currency`, `navPosition`, `navSection`, `scope`, `verticalId`, `dataProvider`, `menuItemLookup`.
44
+
45
+ ## Part of the Fayz SDK
46
+ A composable plugin for the Fayz SDK. The catalog `@fayz-ai/plugin-orders` reads from — pair both with `@fayz-ai/plugin-inventory` to tie menu items back to stock.
47
+
48
+ ## Roadmap & contributing
49
+ Built and evolving in the open. See the [Fayz SDK roadmap](../../docs/ROADMAP.md#plugin-menu) for current gaps, missing features, and good first issues.
@@ -0,0 +1,30 @@
1
+ import { formatCurrency, type CurrencyConfig } from '@fayz-ai/saas';
2
+ import type { MenuDataProvider } from './data/types';
3
+ import type { MenuUIState } from './store';
4
+ export interface MenuPluginLabels {
5
+ pageTitle: string;
6
+ menuManager: string;
7
+ categories: string;
8
+ newItem: string;
9
+ }
10
+ export interface MenuModules {
11
+ modifiers: boolean;
12
+ deliveryPricing: boolean;
13
+ }
14
+ export type MenuCurrency = CurrencyConfig;
15
+ export interface ResolvedMenuConfig {
16
+ modules: MenuModules;
17
+ labels: MenuPluginLabels;
18
+ currency: MenuCurrency;
19
+ }
20
+ export declare const MenuContextProvider: import("react").FC<{
21
+ config: ResolvedMenuConfig;
22
+ provider: MenuDataProvider;
23
+ store: import("zustand").StoreApi<MenuUIState>;
24
+ children?: React.ReactNode;
25
+ }>;
26
+ export declare const useMenuConfig: () => ResolvedMenuConfig;
27
+ export declare const useMenuProvider: () => MenuDataProvider;
28
+ export declare const useMenuStore: <T>(selector: (state: MenuUIState) => T) => T;
29
+ export { formatCurrency };
30
+ //# sourceMappingURL=MenuContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MenuContext.d.ts","sourceRoot":"","sources":["../src/MenuContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACxF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAM1C,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;CAChB;AAMD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,OAAO,CAAA;IAClB,eAAe,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,MAAM,YAAY,GAAG,cAAc,CAAA;AAEzC,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,WAAW,CAAA;IACpB,MAAM,EAAE,gBAAgB,CAAA;IACxB,QAAQ,EAAE,YAAY,CAAA;CACvB;AAID,eAAO,MAAM,mBAAmB;;;;;EAAsB,CAAA;AACtD,eAAO,MAAM,aAAa,0BAAgB,CAAA;AAC1C,eAAO,MAAM,eAAe,wBAAkB,CAAA;AAC9C,eAAO,MAAM,YAAY,+CAAe,CAAA;AAExC,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,520 @@
1
+ import { createPluginContext, formatCurrency } from '@fayz-ai/saas';
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import { Loader2, Plus, Search, UtensilsCrossed, GripVertical, ChevronDown, ChevronRight, MoreHorizontal, Star } from 'lucide-react';
4
+ import { Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, Input, ModalFooter, Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@fayz-ai/ui';
5
+ import { useTranslation } from '@fayz-ai/core';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+
8
+ // src/MenuContext.tsx
9
+ var ctx = createPluginContext("MenuPage");
10
+ var MenuContextProvider = ctx.ContextProvider;
11
+ var useMenuConfig = ctx.useConfig;
12
+ ctx.useProvider;
13
+ var useMenuStore = ctx.useStore;
14
+ var UNCATEGORIZED = "__none";
15
+ var fieldLabel = "text-sm font-medium text-foreground";
16
+ function CategoryModal({ open, onClose }) {
17
+ const t = useTranslation();
18
+ const createCategory = useMenuStore((s) => s.createCategory);
19
+ const [name, setName] = useState("");
20
+ const [saving, setSaving] = useState(false);
21
+ const submit = async () => {
22
+ if (!name.trim()) return;
23
+ setSaving(true);
24
+ try {
25
+ await createCategory({ name: name.trim() });
26
+ setName("");
27
+ onClose();
28
+ } catch {
29
+ } finally {
30
+ setSaving(false);
31
+ }
32
+ };
33
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
34
+ if (!o) onClose();
35
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "sm", children: [
36
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: t("menu.manager.addCategory") }) }),
37
+ /* @__PURE__ */ jsx(ModalBody, { className: "space-y-4", children: /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
38
+ /* @__PURE__ */ jsx("span", { className: fieldLabel, children: t("menu.form.categoryName") }),
39
+ /* @__PURE__ */ jsx(
40
+ Input,
41
+ {
42
+ autoFocus: true,
43
+ value: name,
44
+ onChange: (e) => setName(e.target.value),
45
+ onKeyDown: (e) => {
46
+ if (e.key === "Enter") submit();
47
+ }
48
+ }
49
+ )
50
+ ] }) }),
51
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
52
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("menu.form.cancel") }),
53
+ /* @__PURE__ */ jsxs(Button, { onClick: submit, disabled: saving || !name.trim(), children: [
54
+ saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
55
+ t("menu.form.save")
56
+ ] })
57
+ ] })
58
+ ] }) });
59
+ }
60
+ function ItemModal({
61
+ open,
62
+ categories,
63
+ defaultCategoryId,
64
+ onClose
65
+ }) {
66
+ const t = useTranslation();
67
+ const config = useMenuConfig();
68
+ const createMenuItem = useMenuStore((s) => s.createMenuItem);
69
+ const [name, setName] = useState("");
70
+ const [price, setPrice] = useState("");
71
+ const [categoryId, setCategoryId] = useState(defaultCategoryId ?? UNCATEGORIZED);
72
+ const [description, setDescription] = useState("");
73
+ const [saving, setSaving] = useState(false);
74
+ const submit = async () => {
75
+ if (!name.trim()) return;
76
+ setSaving(true);
77
+ try {
78
+ await createMenuItem({
79
+ name: name.trim(),
80
+ price: Number(price) || 0,
81
+ categoryId: categoryId === UNCATEGORIZED ? void 0 : categoryId,
82
+ description: description.trim() || void 0
83
+ });
84
+ onClose();
85
+ } catch {
86
+ } finally {
87
+ setSaving(false);
88
+ }
89
+ };
90
+ return /* @__PURE__ */ jsx(Modal, { open, onOpenChange: (o) => {
91
+ if (!o) onClose();
92
+ }, children: /* @__PURE__ */ jsxs(ModalContent, { size: "md", children: [
93
+ /* @__PURE__ */ jsx(ModalHeader, { children: /* @__PURE__ */ jsx(ModalTitle, { children: t("menu.manager.addProduct") }) }),
94
+ /* @__PURE__ */ jsxs(ModalBody, { className: "space-y-4", children: [
95
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
96
+ /* @__PURE__ */ jsx("span", { className: fieldLabel, children: t("menu.form.itemName") }),
97
+ /* @__PURE__ */ jsx(Input, { autoFocus: true, value: name, onChange: (e) => setName(e.target.value) })
98
+ ] }),
99
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
100
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
101
+ /* @__PURE__ */ jsxs("span", { className: fieldLabel, children: [
102
+ t("menu.manager.price"),
103
+ " (",
104
+ config.currency.symbol,
105
+ ")"
106
+ ] }),
107
+ /* @__PURE__ */ jsx(
108
+ Input,
109
+ {
110
+ type: "number",
111
+ min: 0,
112
+ step: "0.01",
113
+ value: price,
114
+ onChange: (e) => setPrice(e.target.value),
115
+ placeholder: "0"
116
+ }
117
+ )
118
+ ] }),
119
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
120
+ /* @__PURE__ */ jsx("span", { className: fieldLabel, children: t("menu.form.category") }),
121
+ /* @__PURE__ */ jsxs(Select, { value: categoryId, onValueChange: setCategoryId, children: [
122
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
123
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
124
+ /* @__PURE__ */ jsx(SelectItem, { value: UNCATEGORIZED, children: t("menu.form.uncategorized") }),
125
+ categories.map((cat) => /* @__PURE__ */ jsx(SelectItem, { value: cat.id, children: cat.name }, cat.id))
126
+ ] })
127
+ ] })
128
+ ] })
129
+ ] }),
130
+ /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
131
+ /* @__PURE__ */ jsxs("span", { className: fieldLabel, children: [
132
+ t("menu.form.description"),
133
+ " ",
134
+ /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground font-normal", children: [
135
+ "(",
136
+ t("menu.form.optional"),
137
+ ")"
138
+ ] })
139
+ ] }),
140
+ /* @__PURE__ */ jsx(
141
+ "textarea",
142
+ {
143
+ value: description,
144
+ onChange: (e) => setDescription(e.target.value),
145
+ rows: 2,
146
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
147
+ }
148
+ )
149
+ ] })
150
+ ] }),
151
+ /* @__PURE__ */ jsxs(ModalFooter, { children: [
152
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, children: t("menu.form.cancel") }),
153
+ /* @__PURE__ */ jsxs(Button, { onClick: submit, disabled: saving || !name.trim(), children: [
154
+ saving && /* @__PURE__ */ jsx(Loader2, { className: "mr-1.5 h-4 w-4 animate-spin" }),
155
+ t("menu.form.save")
156
+ ] })
157
+ ] })
158
+ ] }) });
159
+ }
160
+ function Toggle({
161
+ checked,
162
+ onChange,
163
+ size = "sm"
164
+ }) {
165
+ const h = size === "xs" ? "h-4 w-7" : "h-5 w-9";
166
+ const dot = size === "xs" ? "h-3 w-3" : "h-4 w-4";
167
+ const translate = size === "xs" ? "translate-x-[13px]" : "translate-x-[18px]";
168
+ return /* @__PURE__ */ jsx(
169
+ "button",
170
+ {
171
+ type: "button",
172
+ onClick: () => onChange(!checked),
173
+ className: `relative inline-flex ${h} items-center rounded-full transition-colors ${checked ? "bg-primary" : "bg-muted-foreground/30"}`,
174
+ children: /* @__PURE__ */ jsx(
175
+ "span",
176
+ {
177
+ className: `pointer-events-none block ${dot} rounded-full bg-white shadow-sm transition-transform ${checked ? translate : "translate-x-[2px]"}`
178
+ }
179
+ )
180
+ }
181
+ );
182
+ }
183
+ function CategorySkeleton() {
184
+ return /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card animate-pulse", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
185
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-4 rounded bg-muted" }),
186
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-4 rounded bg-muted" }),
187
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-32 rounded bg-muted flex-1" }),
188
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-16 rounded bg-muted" })
189
+ ] }) });
190
+ }
191
+ function CategorySection({
192
+ category,
193
+ items,
194
+ expanded,
195
+ onToggle,
196
+ onAddProduct
197
+ }) {
198
+ const t = useTranslation();
199
+ const config = useMenuConfig();
200
+ const toggleAvailability = useMenuStore((s) => s.toggleAvailability);
201
+ const handleToggle = async (id, field, current) => {
202
+ await toggleAvailability(id, field, !current);
203
+ };
204
+ const handleFeatureToggle = async (id, current) => {
205
+ await toggleAvailability(id, "isFeatured", !current);
206
+ };
207
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card", children: [
208
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
209
+ /* @__PURE__ */ jsx(GripVertical, { className: "h-4 w-4 text-muted-foreground/40 cursor-grab" }),
210
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onToggle, className: "p-0.5", children: expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }) }),
211
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold flex-1", children: category.name }),
212
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-4 text-xs text-muted-foreground", children: /* @__PURE__ */ jsx("span", { children: t("menu.manager.itemsCount", { count: items.length }) }) }),
213
+ /* @__PURE__ */ jsx("button", { className: "p-1 text-muted-foreground hover:text-foreground rounded", children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" }) })
214
+ ] }),
215
+ expanded && /* @__PURE__ */ jsxs(Fragment, { children: [
216
+ /* @__PURE__ */ jsx("div", { className: "px-4 pb-2", children: /* @__PURE__ */ jsxs(
217
+ "button",
218
+ {
219
+ type: "button",
220
+ onClick: onAddProduct,
221
+ className: "flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground border border-dashed rounded-md px-3 py-1.5 hover:bg-muted/30 transition-colors",
222
+ children: [
223
+ /* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }),
224
+ t("menu.manager.addProduct")
225
+ ]
226
+ }
227
+ ) }),
228
+ items.length > 0 ? /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
229
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-t bg-muted/30 text-[10px] uppercase tracking-wider text-muted-foreground", children: [
230
+ /* @__PURE__ */ jsx("th", { className: "text-left py-2 pl-4 pr-2 font-medium", children: t("menu.manager.item") }),
231
+ /* @__PURE__ */ jsx("th", { className: "py-2 px-2 font-medium text-center w-16", children: t("menu.manager.pos") }),
232
+ /* @__PURE__ */ jsx("th", { className: "py-2 px-2 font-medium text-center w-16", children: t("menu.manager.delivery") }),
233
+ /* @__PURE__ */ jsx("th", { className: "py-2 px-2 font-medium text-center w-28", children: t("menu.manager.price") }),
234
+ config.modules.deliveryPricing && /* @__PURE__ */ jsx("th", { className: "py-2 px-2 font-medium text-center w-28", children: t("menu.manager.deliveryPrice") }),
235
+ /* @__PURE__ */ jsx("th", { className: "py-2 px-2 font-medium text-center w-16", children: t("menu.manager.featured") }),
236
+ /* @__PURE__ */ jsx("th", { className: "py-2 pr-4 w-8" })
237
+ ] }) }),
238
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y", children: items.map((item) => /* @__PURE__ */ jsxs(
239
+ "tr",
240
+ {
241
+ className: "hover:bg-muted/20 transition-colors",
242
+ children: [
243
+ /* @__PURE__ */ jsx("td", { className: "py-2.5 pl-4 pr-2", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
244
+ /* @__PURE__ */ jsx(GripVertical, { className: "h-3.5 w-3.5 text-muted-foreground/30 cursor-grab shrink-0" }),
245
+ /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-muted/50 overflow-hidden", children: item.imageUrl ? /* @__PURE__ */ jsx(
246
+ "img",
247
+ {
248
+ src: item.imageUrl,
249
+ alt: "",
250
+ className: "h-full w-full object-cover"
251
+ }
252
+ ) : /* @__PURE__ */ jsx(UtensilsCrossed, { className: "h-4 w-4 text-muted-foreground/40" }) }),
253
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
254
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", children: item.name }),
255
+ item.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground truncate max-w-[280px]", children: item.description })
256
+ ] })
257
+ ] }) }),
258
+ /* @__PURE__ */ jsx("td", { className: "py-2.5 px-2 text-center", children: /* @__PURE__ */ jsx(
259
+ Toggle,
260
+ {
261
+ checked: item.availableForPos,
262
+ onChange: () => handleToggle(
263
+ item.id,
264
+ "availableForPos",
265
+ item.availableForPos
266
+ ),
267
+ size: "xs"
268
+ }
269
+ ) }),
270
+ /* @__PURE__ */ jsx("td", { className: "py-2.5 px-2 text-center", children: /* @__PURE__ */ jsx(
271
+ Toggle,
272
+ {
273
+ checked: item.availableForDelivery,
274
+ onChange: () => handleToggle(
275
+ item.id,
276
+ "availableForDelivery",
277
+ item.availableForDelivery
278
+ ),
279
+ size: "xs"
280
+ }
281
+ ) }),
282
+ /* @__PURE__ */ jsx("td", { className: "py-2.5 px-2 text-center", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: formatCurrency(item.price, config.currency) }) }),
283
+ config.modules.deliveryPricing && /* @__PURE__ */ jsx("td", { className: "py-2.5 px-2 text-center", children: /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: item.deliveryPrice != null ? formatCurrency(
284
+ item.deliveryPrice,
285
+ config.currency
286
+ ) : "\u2014" }) }),
287
+ /* @__PURE__ */ jsx("td", { className: "py-2.5 px-2 text-center", children: /* @__PURE__ */ jsx(
288
+ "button",
289
+ {
290
+ onClick: () => handleFeatureToggle(item.id, item.isFeatured),
291
+ className: `p-1 rounded ${item.isFeatured ? "text-amber-500" : "text-muted-foreground/30 hover:text-muted-foreground"}`,
292
+ children: /* @__PURE__ */ jsx(
293
+ Star,
294
+ {
295
+ className: `h-4 w-4 ${item.isFeatured ? "fill-current" : ""}`
296
+ }
297
+ )
298
+ }
299
+ ) }),
300
+ /* @__PURE__ */ jsx("td", { className: "py-2.5 pr-4", children: /* @__PURE__ */ jsx("button", { className: "p-1 text-muted-foreground hover:text-foreground rounded", children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-3.5 w-3.5" }) }) })
301
+ ]
302
+ },
303
+ item.id
304
+ )) })
305
+ ] }) }) : /* @__PURE__ */ jsx("div", { className: "border-t px-4 py-6 text-center text-xs text-muted-foreground", children: t("menu.manager.noItems") })
306
+ ] })
307
+ ] });
308
+ }
309
+ function MenuManagerView() {
310
+ const t = useTranslation();
311
+ useMenuConfig();
312
+ const categories = useMenuStore((s) => s.categories);
313
+ const categoriesLoading = useMenuStore((s) => s.categoriesLoading);
314
+ const menuItems = useMenuStore((s) => s.menuItems);
315
+ const menuItemsLoading = useMenuStore((s) => s.menuItemsLoading);
316
+ const fetchCategories = useMenuStore((s) => s.fetchCategories);
317
+ const fetchMenuItems = useMenuStore((s) => s.fetchMenuItems);
318
+ const [expandedCategories, setExpandedCategories] = useState(
319
+ /* @__PURE__ */ new Set()
320
+ );
321
+ const [search, setSearch] = useState("");
322
+ const [categoryFilter, setCategoryFilter] = useState("all");
323
+ const [showCategoryModal, setShowCategoryModal] = useState(false);
324
+ const [showItemModal, setShowItemModal] = useState(false);
325
+ const [itemModalCategory, setItemModalCategory] = useState(void 0);
326
+ const openItemModal = (categoryId) => {
327
+ setItemModalCategory(categoryId);
328
+ setShowItemModal(true);
329
+ };
330
+ useEffect(() => {
331
+ fetchCategories();
332
+ fetchMenuItems({});
333
+ }, []);
334
+ useEffect(() => {
335
+ if (categories.length > 0 && expandedCategories.size === 0) {
336
+ setExpandedCategories(new Set(categories.map((c) => c.id)));
337
+ }
338
+ }, [categories]);
339
+ const grouped = useMemo(() => {
340
+ const map = /* @__PURE__ */ new Map();
341
+ for (const item of menuItems) {
342
+ const key = item.categoryId ?? "__uncategorized";
343
+ const list = map.get(key) ?? [];
344
+ list.push(item);
345
+ map.set(key, list);
346
+ }
347
+ return map;
348
+ }, [menuItems]);
349
+ const filteredCategories = useMemo(() => {
350
+ const lowerSearch = search.toLowerCase();
351
+ return categories.filter((cat) => {
352
+ if (categoryFilter !== "all" && cat.id !== categoryFilter) return false;
353
+ if (lowerSearch) {
354
+ const items = grouped.get(cat.id) ?? [];
355
+ return items.some(
356
+ (item) => item.name.toLowerCase().includes(lowerSearch) || (item.description?.toLowerCase().includes(lowerSearch) ?? false)
357
+ );
358
+ }
359
+ return true;
360
+ });
361
+ }, [categories, categoryFilter, search, grouped]);
362
+ const getFilteredItems = (categoryId) => {
363
+ const items = grouped.get(categoryId) ?? [];
364
+ if (!search) return items;
365
+ const lowerSearch = search.toLowerCase();
366
+ return items.filter(
367
+ (item) => item.name.toLowerCase().includes(lowerSearch) || (item.description?.toLowerCase().includes(lowerSearch) ?? false)
368
+ );
369
+ };
370
+ const toggleCategory = (id) => {
371
+ setExpandedCategories((prev) => {
372
+ const next = new Set(prev);
373
+ next.has(id) ? next.delete(id) : next.add(id);
374
+ return next;
375
+ });
376
+ };
377
+ const isLoading = categoriesLoading || menuItemsLoading;
378
+ if (isLoading && categories.length === 0) {
379
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
380
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsxs("div", { children: [
381
+ /* @__PURE__ */ jsx("div", { className: "h-7 w-40 rounded bg-muted animate-pulse" }),
382
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-64 rounded bg-muted animate-pulse mt-2" })
383
+ ] }) }),
384
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
385
+ /* @__PURE__ */ jsx("div", { className: "h-9 w-40 rounded bg-muted animate-pulse" }),
386
+ /* @__PURE__ */ jsx("div", { className: "h-9 flex-1 max-w-sm rounded bg-muted animate-pulse" })
387
+ ] }),
388
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
389
+ /* @__PURE__ */ jsx(CategorySkeleton, {}),
390
+ /* @__PURE__ */ jsx(CategorySkeleton, {}),
391
+ /* @__PURE__ */ jsx(CategorySkeleton, {})
392
+ ] })
393
+ ] });
394
+ }
395
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
396
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
397
+ /* @__PURE__ */ jsxs("div", { children: [
398
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold", children: t("menu.manager.title") }),
399
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("menu.manager.subtitle") })
400
+ ] }),
401
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
402
+ isLoading && /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" }),
403
+ /* @__PURE__ */ jsxs(
404
+ "button",
405
+ {
406
+ type: "button",
407
+ onClick: () => setShowCategoryModal(true),
408
+ className: "inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-muted/50 transition-colors",
409
+ children: [
410
+ /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
411
+ t("menu.manager.addCategory")
412
+ ]
413
+ }
414
+ ),
415
+ /* @__PURE__ */ jsxs(
416
+ "button",
417
+ {
418
+ type: "button",
419
+ onClick: () => openItemModal(void 0),
420
+ className: "inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors",
421
+ children: [
422
+ /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
423
+ t("menu.manager.addProduct")
424
+ ]
425
+ }
426
+ )
427
+ ] })
428
+ ] }),
429
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
430
+ /* @__PURE__ */ jsxs(
431
+ "select",
432
+ {
433
+ value: categoryFilter,
434
+ onChange: (e) => setCategoryFilter(e.target.value),
435
+ className: "h-9 rounded-md border border-input bg-background px-3 text-sm",
436
+ children: [
437
+ /* @__PURE__ */ jsx("option", { value: "all", children: t("menu.manager.allCategories") }),
438
+ categories.map((cat) => /* @__PURE__ */ jsx("option", { value: cat.id, children: cat.name }, cat.id))
439
+ ]
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 max-w-sm", children: [
443
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }),
444
+ /* @__PURE__ */ jsx(
445
+ "input",
446
+ {
447
+ type: "text",
448
+ value: search,
449
+ onChange: (e) => setSearch(e.target.value),
450
+ placeholder: t("menu.manager.search"),
451
+ className: "h-9 w-full rounded-md border border-input bg-background pl-9 pr-3 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
452
+ }
453
+ )
454
+ ] })
455
+ ] }),
456
+ /* @__PURE__ */ jsx("div", { className: "space-y-3", children: filteredCategories.map((category) => /* @__PURE__ */ jsx(
457
+ CategorySection,
458
+ {
459
+ category,
460
+ items: getFilteredItems(category.id),
461
+ expanded: expandedCategories.has(category.id),
462
+ onToggle: () => toggleCategory(category.id),
463
+ onAddProduct: () => openItemModal(category.id)
464
+ },
465
+ category.id
466
+ )) }),
467
+ filteredCategories.length === 0 && !isLoading && /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: categories.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
468
+ /* @__PURE__ */ jsx(UtensilsCrossed, { className: "mx-auto h-10 w-10 text-muted-foreground/40 mb-3" }),
469
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("menu.manager.noItems") }),
470
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1", children: t("menu.manager.noItemsDesc") }),
471
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-center justify-center gap-2", children: [
472
+ /* @__PURE__ */ jsxs(
473
+ "button",
474
+ {
475
+ type: "button",
476
+ onClick: () => setShowCategoryModal(true),
477
+ className: "inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-muted/50",
478
+ children: [
479
+ /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
480
+ t("menu.manager.addCategory")
481
+ ]
482
+ }
483
+ ),
484
+ /* @__PURE__ */ jsxs(
485
+ "button",
486
+ {
487
+ type: "button",
488
+ onClick: () => openItemModal(void 0),
489
+ className: "inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90",
490
+ children: [
491
+ /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
492
+ t("menu.manager.addProduct")
493
+ ]
494
+ }
495
+ )
496
+ ] })
497
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
498
+ /* @__PURE__ */ jsx(Search, { className: "mx-auto h-10 w-10 text-muted-foreground/40 mb-3" }),
499
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t("menu.manager.noResults") }),
500
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1", children: t("menu.manager.noResultsDesc") })
501
+ ] }) }),
502
+ showCategoryModal && /* @__PURE__ */ jsx(CategoryModal, { open: true, onClose: () => setShowCategoryModal(false) }),
503
+ showItemModal && /* @__PURE__ */ jsx(
504
+ ItemModal,
505
+ {
506
+ open: true,
507
+ categories,
508
+ defaultCategoryId: itemModalCategory,
509
+ onClose: () => setShowItemModal(false)
510
+ }
511
+ )
512
+ ] });
513
+ }
514
+ function MenuPage({ config, provider, store, registries }) {
515
+ return /* @__PURE__ */ jsx(MenuContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(MenuManagerView, {}) });
516
+ }
517
+
518
+ export { MenuPage };
519
+ //# sourceMappingURL=MenuPage-OABV6JYB.js.map
520
+ //# sourceMappingURL=MenuPage-OABV6JYB.js.map