@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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/MenuContext.d.ts +30 -0
- package/dist/MenuContext.d.ts.map +1 -0
- package/dist/MenuPage-OABV6JYB.js +520 -0
- package/dist/MenuPage-OABV6JYB.js.map +1 -0
- package/dist/MenuPage-VMI2YAPY.cjs +522 -0
- package/dist/MenuPage-VMI2YAPY.cjs.map +1 -0
- package/dist/MenuPage.d.ts +13 -0
- package/dist/MenuPage.d.ts.map +1 -0
- package/dist/data/fayz.d.ts +12 -0
- package/dist/data/fayz.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/types.d.ts +22 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +1053 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1046 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +26 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/MenuManagerView.d.ts +3 -0
- package/dist/views/MenuManagerView.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/MenuContext.tsx +40 -0
- package/src/MenuPage.tsx +21 -0
- package/src/data/fayz.ts +429 -0
- package/src/data/mock.ts +289 -0
- package/src/data/types.ts +33 -0
- package/src/index.ts +184 -0
- package/src/locales/en.ts +64 -0
- package/src/locales/index.ts +4 -0
- package/src/locales/pt-BR.ts +64 -0
- package/src/registries.ts +84 -0
- package/src/store.ts +161 -0
- package/src/types.ts +145 -0
- 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
|
+
[](https://www.npmjs.com/package/@fayz-ai/plugin-menu)
|
|
6
|
+
[](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
|