@fayz-ai/plugin-inventory 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var React11 = require('react');
3
+ var React2 = require('react');
4
4
  var ui = require('@fayz-ai/ui');
5
5
  var saas = require('@fayz-ai/saas');
6
6
  var core = require('@fayz-ai/core');
@@ -10,7 +10,7 @@ var vanilla = require('zustand/vanilla');
10
10
 
11
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
12
 
13
- var React11__default = /*#__PURE__*/_interopDefault(React11);
13
+ var React2__default = /*#__PURE__*/_interopDefault(React2);
14
14
 
15
15
  // src/index.ts
16
16
  var ctx = saas.createPluginContext("InventoryPage");
@@ -21,339 +21,238 @@ var useInventoryStore = ctx.useStore;
21
21
  function DashboardView() {
22
22
  return /* @__PURE__ */ jsxRuntime.jsx(ui.DashboardCanvas, { surface: "plugin-home", domain: "inventory", showHeader: false, className: "space-y-6" });
23
23
  }
24
- var useColumns = (currency, t2) => [
25
- {
26
- accessorKey: "name",
27
- header: t2("inventory.productList.product"),
28
- enableSorting: true,
29
- cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
30
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: row.original.name }),
31
- row.original.sku && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: row.original.sku })
32
- ] })
33
- },
34
- {
35
- accessorKey: "productType",
36
- header: t2("inventory.productList.type"),
37
- cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium bg-muted text-muted-foreground capitalize", children: getValue() })
38
- },
39
- {
40
- accessorKey: "currentQuantity",
41
- header: t2("inventory.productList.stock"),
42
- enableSorting: true,
43
- meta: { align: "right" },
44
- cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-right block ${row.original.currentQuantity <= row.original.minQuantity ? "text-destructive font-medium" : ""}`, children: row.original.currentQuantity })
45
- },
46
- {
47
- accessorKey: "costPrice",
48
- header: t2("inventory.productList.cost"),
49
- meta: { align: "right" },
50
- cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-muted-foreground", children: saas.formatCurrency(getValue(), currency) })
51
- },
52
- {
53
- id: "value",
54
- header: t2("inventory.productList.value"),
55
- meta: { align: "right" },
56
- cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block font-medium", children: saas.formatCurrency(row.original.currentQuantity * row.original.costPrice, currency) })
57
- }
58
- ];
24
+ var TYPE_DESCRIPTION_KEYS = {
25
+ ingredient: "inventory.productForm.typeIngredient",
26
+ sale: "inventory.productForm.typeSale",
27
+ intermediate: "inventory.productForm.typeIntermediate",
28
+ asset: "inventory.productForm.typeAsset"
29
+ };
30
+ function buildProductEntity(t2, productTypes, currency) {
31
+ const typeOptions = productTypes.map((pt) => ({
32
+ label: pt.label,
33
+ value: pt.value,
34
+ description: TYPE_DESCRIPTION_KEYS[pt.value] ? t2(TYPE_DESCRIPTION_KEYS[pt.value]) : void 0
35
+ }));
36
+ const typeLabel = (v) => productTypes.find((p) => p.value === v)?.label ?? String(v ?? "");
37
+ const fields = [
38
+ // — General Information (form) + Product column (table) —
39
+ {
40
+ key: "name",
41
+ label: t2("inventory.productForm.name"),
42
+ type: "text",
43
+ required: true,
44
+ group: "general",
45
+ placeholder: t2("inventory.productForm.namePlaceholder"),
46
+ searchable: true,
47
+ showInTable: true,
48
+ sortable: true,
49
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
50
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: row.name }),
51
+ row.sku && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: row.sku })
52
+ ] })
53
+ },
54
+ { key: "brand", label: t2("inventory.productForm.brand"), type: "text", group: "general", placeholder: t2("inventory.productForm.brandPlaceholder"), showInTable: false },
55
+ { key: "sku", label: t2("inventory.productForm.sku"), type: "text", group: "general", placeholder: t2("inventory.productForm.skuPlaceholder"), searchable: true, showInTable: false },
56
+ { key: "barcode", label: t2("inventory.productForm.barcode"), type: "text", group: "general", placeholder: t2("inventory.productForm.barcodePlaceholder"), searchable: true, showInTable: false },
57
+ { key: "description", label: t2("inventory.productForm.description"), type: "textarea", group: "general", span: 2, placeholder: t2("inventory.productForm.descriptionPlaceholder"), showInTable: false },
58
+ // — Classification (form: segmented) + Type column (table: badge) —
59
+ {
60
+ key: "productType",
61
+ label: t2("inventory.productForm.classification"),
62
+ type: "segmented",
63
+ group: "classification",
64
+ span: 2,
65
+ options: typeOptions,
66
+ showInTable: true,
67
+ sortable: false,
68
+ renderCell: (v) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium bg-muted text-muted-foreground", children: typeLabel(v) })
69
+ },
70
+ // — Stock column (list only) — placed before Cost for the desired column order
71
+ {
72
+ key: "currentQuantity",
73
+ label: t2("inventory.productList.stock"),
74
+ type: "number",
75
+ showInForm: false,
76
+ showInTable: true,
77
+ sortable: true,
78
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-right block ${row.currentQuantity <= row.minQuantity ? "text-destructive font-medium" : ""}`, children: row.currentQuantity })
79
+ },
80
+ // — Pricing (form) + Cost column (table) —
81
+ {
82
+ key: "costPrice",
83
+ label: t2("inventory.productForm.costPrice"),
84
+ type: "currency",
85
+ group: "pricing",
86
+ currency: currency.code,
87
+ currencySymbol: currency.symbol,
88
+ currencyLocale: currency.locale,
89
+ showInTable: true,
90
+ sortable: false,
91
+ renderCell: (v) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-muted-foreground", children: saas.formatCurrency(Number(v) || 0, currency) })
92
+ },
93
+ // — Value column (list only): stock × cost —
94
+ {
95
+ key: "value",
96
+ label: t2("inventory.productList.value"),
97
+ type: "number",
98
+ showInForm: false,
99
+ showInTable: true,
100
+ sortable: false,
101
+ renderCell: (_v, row) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block font-medium", children: saas.formatCurrency((row.currentQuantity || 0) * (row.costPrice || 0), currency) })
102
+ },
103
+ { key: "salePrice", label: t2("inventory.productForm.salePrice"), type: "currency", group: "pricing", currency: currency.code, currencySymbol: currency.symbol, currencyLocale: currency.locale, showInTable: false },
104
+ {
105
+ key: "margin",
106
+ label: t2("inventory.productForm.margin"),
107
+ type: "computed",
108
+ group: "pricing",
109
+ showInTable: false,
110
+ compute: (vals) => {
111
+ const cost = Number(vals.costPrice) || 0;
112
+ const sale = Number(vals.salePrice) || 0;
113
+ if (sale <= 0 || cost <= 0) return null;
114
+ const m = (sale - cost) / cost * 100;
115
+ return { display: `${m.toFixed(1)}%`, tone: m >= 0 ? "positive" : "negative" };
116
+ }
117
+ },
118
+ // — Stock Levels (form only) —
119
+ { key: "minQuantity", label: t2("inventory.productForm.minQuantity"), type: "number", group: "stock", min: 0, placeholder: "0", hint: t2("inventory.productForm.minQuantityHint"), showInTable: false },
120
+ { key: "maxQuantity", label: t2("inventory.productForm.maxQuantity"), type: "number", group: "stock", min: 0, placeholder: t2("inventory.productForm.optional"), hint: t2("inventory.productForm.maxQuantityHint"), showInTable: false }
121
+ ];
122
+ return {
123
+ name: t2("inventory.stock.product"),
124
+ namePlural: t2("inventory.nav.products"),
125
+ icon: "Package",
126
+ displayField: "name",
127
+ fields,
128
+ fieldGroups: [
129
+ { id: "general", label: t2("inventory.productForm.generalInfo"), columns: 2, imageSlot: true },
130
+ { id: "classification", label: t2("inventory.productForm.classification"), description: t2("inventory.productForm.classificationDesc"), columns: 1 },
131
+ { id: "pricing", label: t2("inventory.productForm.pricing"), columns: 3 },
132
+ { id: "stock", label: t2("inventory.productForm.stockLevels"), description: t2("inventory.productForm.stockLevelsDesc"), columns: 2 }
133
+ ],
134
+ facets: [{ field: "productType", allLabel: t2("crud.list.allFacet") }]
135
+ };
136
+ }
59
137
  function ProductListView({ onNew, onEdit }) {
60
138
  const t2 = core.useTranslation();
61
139
  const { currency, productTypes } = useInventoryConfig();
62
140
  const products = useInventoryStore((s) => s.products);
63
141
  const productsTotal = useInventoryStore((s) => s.productsTotal);
64
- const productsLoading = useInventoryStore((s) => s.productsLoading);
142
+ useInventoryStore((s) => s.productsLoading);
65
143
  const fetchProducts = useInventoryStore((s) => s.fetchProducts);
66
- const [search, setSearch] = React11.useState("");
67
- const [typeFilter, setTypeFilter] = React11.useState();
68
- React11.useEffect(() => {
69
- fetchProducts({ productType: typeFilter, search: search || void 0 });
144
+ const [search, setSearch] = React2.useState("");
145
+ const [typeFilter, setTypeFilter] = React2.useState();
146
+ const [loadedOnce, setLoadedOnce] = React2.useState(false);
147
+ React2.useEffect(() => {
148
+ let active = true;
149
+ Promise.resolve(fetchProducts({ productType: typeFilter, search: search || void 0 })).finally(() => {
150
+ if (active) setLoadedOnce(true);
151
+ });
152
+ return () => {
153
+ active = false;
154
+ };
70
155
  }, [typeFilter, search]);
71
- const columns = useColumns(currency, t2);
72
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
73
- /* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: t2("inventory.productList.title"), subtitle: t2("inventory.productList.subtitle", { count: String(productsTotal) }) }),
74
- /* @__PURE__ */ jsxRuntime.jsx(
75
- ui.ListView,
76
- {
77
- columns,
78
- data: products,
79
- loading: productsLoading,
80
- searchPlaceholder: t2("inventory.productList.searchPlaceholder"),
81
- search,
82
- onSearchChange: setSearch,
83
- searchDebounce: 0,
84
- tags: productTypes.map((t3) => ({ value: t3.value, label: t3.label })),
85
- activeTag: typeFilter,
86
- onTagChange: setTypeFilter,
87
- newLabel: t2("inventory.productList.newProduct"),
88
- onNew,
89
- onRowClick: (row) => onEdit?.(row.id),
90
- emptyIcon: lucideReact.Package,
91
- emptyMessage: t2("inventory.productList.empty"),
92
- emptyActionLabel: onNew ? t2("inventory.productList.createFirst") : void 0,
93
- onEmptyAction: onNew
94
- }
95
- )
96
- ] });
97
- }
98
- var TYPE_DESCRIPTION_KEYS = {
99
- ingredient: "inventory.productForm.typeIngredient",
100
- sale: "inventory.productForm.typeSale",
101
- intermediate: "inventory.productForm.typeIntermediate",
102
- asset: "inventory.productForm.typeAsset"
103
- };
104
- function FormSection({ title, subtitle, children }) {
105
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm", children: [
106
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-5 py-3 border-b", children: [
107
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold", children: title }),
108
- subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: subtitle })
109
- ] }),
110
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-5 space-y-4", children })
111
- ] });
156
+ const entity = React2.useMemo(() => buildProductEntity(t2, productTypes, currency), [t2, productTypes, currency]);
157
+ const facets = React2.useMemo(() => [{
158
+ field: "productType",
159
+ allLabel: t2("crud.list.allFacet"),
160
+ options: productTypes.map((p) => ({ value: p.value, label: p.label }))
161
+ }], [productTypes, t2]);
162
+ return /* @__PURE__ */ jsxRuntime.jsx(
163
+ saas.CrudListView,
164
+ {
165
+ entityDef: entity,
166
+ items: loadedOnce ? products : null,
167
+ total: productsTotal,
168
+ search,
169
+ onSearchChange: setSearch,
170
+ searchPlaceholder: t2("inventory.productList.searchPlaceholder"),
171
+ facets,
172
+ activeFilters: { productType: typeFilter },
173
+ onFacetChange: (_field, value) => setTypeFilter(value),
174
+ onNew,
175
+ addLabel: t2("inventory.productList.newProduct"),
176
+ onRowClick: (row) => onEdit?.(row.id)
177
+ }
178
+ );
112
179
  }
113
- function ProductFormView({ editId, onSaved }) {
180
+ function ProductCrudForm({ editId, onSaved }) {
114
181
  const t2 = core.useTranslation();
115
182
  const { productTypes, currency } = useInventoryConfig();
116
183
  const provider = useInventoryProvider();
117
184
  const createProduct = useInventoryStore((s) => s.createProduct);
118
185
  const isEdit = !!editId;
119
- const [loaded, setLoaded] = React11.useState(false);
120
- const [name, setName] = React11.useState("");
121
- const [sku, setSku] = React11.useState("");
122
- const [barcode, setBarcode] = React11.useState("");
123
- const [brand, setBrand] = React11.useState("");
124
- const [productType, setProductType] = React11.useState(productTypes[0]?.value ?? "ingredient");
125
- const [costPrice, setCostPrice] = React11.useState(0);
126
- const [salePrice, setSalePrice] = React11.useState(0);
127
- const [minQuantity, setMinQuantity] = React11.useState(0);
128
- const [maxQuantity, setMaxQuantity] = React11.useState(0);
129
- const [description, setDescription] = React11.useState("");
130
- const [saving, setSaving] = React11.useState(false);
131
- React11.useEffect(() => {
132
- if (!editId || loaded) return;
186
+ const [initialData, setInitialData] = React2__default.default.useState(isEdit ? null : {});
187
+ const [headerName, setHeaderName] = React2__default.default.useState("");
188
+ React2__default.default.useEffect(() => {
189
+ if (!editId) return;
190
+ let cancelled = false;
133
191
  (async () => {
134
- const product = await provider.getProductById(editId);
135
- if (!product) return;
136
- setName(product.name);
137
- setSku(product.sku ?? "");
138
- setBarcode(product.barcode ?? "");
139
- setBrand(product.brand ?? "");
140
- setProductType(product.productType);
141
- setCostPrice(product.costPrice);
142
- setSalePrice(product.salePrice ?? 0);
143
- setMinQuantity(product.minQuantity);
144
- setMaxQuantity(product.maxQuantity ?? 0);
145
- setDescription(product.description ?? "");
146
- setLoaded(true);
192
+ const p = await provider.getProductById(editId);
193
+ if (cancelled) return;
194
+ setHeaderName(p?.name ?? "");
195
+ setInitialData(
196
+ p ? {
197
+ name: p.name,
198
+ brand: p.brand ?? "",
199
+ sku: p.sku ?? "",
200
+ barcode: p.barcode ?? "",
201
+ description: p.description ?? "",
202
+ productType: p.productType,
203
+ costPrice: p.costPrice,
204
+ salePrice: p.salePrice ?? 0,
205
+ minQuantity: p.minQuantity,
206
+ maxQuantity: p.maxQuantity ?? 0
207
+ } : {}
208
+ );
147
209
  })();
210
+ return () => {
211
+ cancelled = true;
212
+ };
148
213
  }, [editId]);
149
- const fields = { name, sku, barcode, brand, productType, costPrice, salePrice, minQuantity, maxQuantity, description };
150
- const ready = !editId || loaded;
151
- const snapshot = React11__default.default.useRef(null);
152
- React11.useEffect(() => {
153
- if (ready && snapshot.current === null) snapshot.current = JSON.stringify(fields);
154
- }, [ready]);
155
- const dirty = snapshot.current !== null && JSON.stringify(fields) !== snapshot.current;
156
- ui.useSaveBar({
157
- dirty,
158
- saving,
159
- onSave: () => {
160
- void handleSave();
161
- },
162
- onDiscard: () => onSaved?.(),
163
- saveLabel: t2("inventory.productForm.save")
164
- });
165
- async function handleSave() {
166
- if (!name.trim()) {
214
+ const entity = React2__default.default.useMemo(() => buildProductEntity(t2, productTypes, currency), [t2, productTypes, currency]);
215
+ async function save(values) {
216
+ const name = String(values.name ?? "").trim();
217
+ if (!name) {
167
218
  ui.toast.error(t2("common.formIncomplete"));
168
- return;
219
+ throw new Error("name required");
169
220
  }
170
- setSaving(true);
171
- try {
172
- if (isEdit && editId) {
173
- await provider.updateProduct(editId, {
174
- name,
175
- sku: sku || void 0,
176
- barcode: barcode || void 0,
177
- brand: brand || void 0,
178
- productType,
179
- costPrice,
180
- salePrice: salePrice || void 0,
181
- minQuantity,
182
- maxQuantity: maxQuantity || void 0,
183
- description: description || void 0
184
- });
185
- } else {
186
- await createProduct({
187
- name,
188
- sku: sku || void 0,
189
- barcode: barcode || void 0,
190
- brand: brand || void 0,
191
- productType,
192
- costPrice,
193
- salePrice: salePrice || void 0,
194
- minQuantity,
195
- maxQuantity: maxQuantity || void 0,
196
- description: description || void 0
197
- });
198
- }
199
- onSaved?.();
200
- } finally {
201
- setSaving(false);
221
+ const payload = {
222
+ name,
223
+ sku: values.sku || void 0,
224
+ barcode: values.barcode || void 0,
225
+ brand: values.brand || void 0,
226
+ productType: values.productType ?? productTypes[0]?.value,
227
+ costPrice: Number(values.costPrice) || 0,
228
+ salePrice: Number(values.salePrice) || void 0,
229
+ minQuantity: Number(values.minQuantity) || 0,
230
+ maxQuantity: Number(values.maxQuantity) || void 0,
231
+ description: values.description || void 0
232
+ };
233
+ if (isEdit && editId) {
234
+ await provider.updateProduct(editId, payload);
235
+ } else {
236
+ await createProduct(payload);
202
237
  }
238
+ onSaved?.();
203
239
  }
204
- const title = isEdit ? name || t2("inventory.productForm.editProduct") : t2("inventory.productForm.newProduct");
240
+ const title = isEdit ? headerName || t2("inventory.productForm.editProduct") : t2("inventory.productForm.newProduct");
205
241
  const subtitle = isEdit ? void 0 : t2("inventory.productForm.addToCatalog");
206
- const margin = salePrice > 0 && costPrice > 0 ? ((salePrice - costPrice) / costPrice * 100).toFixed(1) : null;
207
242
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
208
- /* @__PURE__ */ jsxRuntime.jsx(
209
- ui.SubpageHeader,
243
+ /* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title, subtitle, onBack: onSaved, parentLabel: t2("inventory.nav.products") }),
244
+ initialData && /* @__PURE__ */ jsxRuntime.jsx(
245
+ saas.CrudFormPage,
210
246
  {
211
- title,
212
- subtitle,
213
- onBack: onSaved,
214
- parentLabel: t2("inventory.nav.products")
247
+ entityDef: entity,
248
+ mode: isEdit ? "edit" : "create",
249
+ initialData,
250
+ onSubmit: save,
251
+ onCancel: () => onSaved?.(),
252
+ namePlural: t2("inventory.nav.products"),
253
+ hideBreadcrumb: true
215
254
  }
216
- ),
217
- /* @__PURE__ */ jsxRuntime.jsxs(FormSection, { title: t2("inventory.productForm.generalInfo"), children: [
218
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4", children: [
219
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-20 h-20 rounded-lg border-2 border-dashed border-muted flex items-center justify-center text-muted-foreground hover:border-primary/30 hover:text-primary/50 transition-colors cursor-pointer", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ImagePlus, { className: "h-5 w-5" }) }) }),
220
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 grid gap-3 sm:grid-cols-2", children: [
221
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sm:col-span-1", children: [
222
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "text-xs font-medium text-muted-foreground", children: [
223
- t2("inventory.productForm.name"),
224
- " *"
225
- ] }),
226
- /* @__PURE__ */ jsxRuntime.jsx(
227
- "input",
228
- {
229
- type: "text",
230
- value: name,
231
- onChange: (e) => setName(e.target.value),
232
- placeholder: t2("inventory.productForm.namePlaceholder"),
233
- autoFocus: true,
234
- className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
235
- }
236
- )
237
- ] }),
238
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
239
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.brand") }),
240
- /* @__PURE__ */ jsxRuntime.jsx(
241
- "input",
242
- {
243
- type: "text",
244
- value: brand,
245
- onChange: (e) => setBrand(e.target.value),
246
- placeholder: t2("inventory.productForm.brandPlaceholder"),
247
- className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
248
- }
249
- )
250
- ] })
251
- ] })
252
- ] }),
253
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
254
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
255
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.sku") }),
256
- /* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", value: sku, onChange: (e) => setSku(e.target.value), placeholder: t2("inventory.productForm.skuPlaceholder"), className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" })
257
- ] }),
258
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
259
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.barcode") }),
260
- /* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", value: barcode, onChange: (e) => setBarcode(e.target.value), placeholder: t2("inventory.productForm.barcodePlaceholder"), className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" })
261
- ] })
262
- ] }),
263
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
264
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.description") }),
265
- /* @__PURE__ */ jsxRuntime.jsx(
266
- "textarea",
267
- {
268
- value: description,
269
- onChange: (e) => setDescription(e.target.value),
270
- rows: 2,
271
- placeholder: t2("inventory.productForm.descriptionPlaceholder"),
272
- className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring resize-none"
273
- }
274
- )
275
- ] })
276
- ] }),
277
- /* @__PURE__ */ jsxRuntime.jsx(FormSection, { title: t2("inventory.productForm.classification"), subtitle: t2("inventory.productForm.classificationDesc"), children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-2 sm:grid-cols-2 lg:grid-cols-4", children: productTypes.map((pt) => {
278
- const active = productType === pt.value;
279
- const descKey = TYPE_DESCRIPTION_KEYS[pt.value];
280
- const desc = descKey ? t2(descKey) : "";
281
- return /* @__PURE__ */ jsxRuntime.jsxs(
282
- "button",
283
- {
284
- onClick: () => setProductType(pt.value),
285
- className: `rounded-lg border-2 p-3 text-left transition-all ${active ? "border-primary bg-primary/5" : "border-transparent bg-muted/20 hover:bg-muted/40"}`,
286
- children: [
287
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm font-medium ${active ? "text-primary" : ""}`, children: pt.label }),
288
- desc && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5 leading-tight", children: desc })
289
- ]
290
- },
291
- pt.value
292
- );
293
- }) }) }),
294
- /* @__PURE__ */ jsxRuntime.jsx(FormSection, { title: t2("inventory.productForm.pricing"), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [
295
- /* @__PURE__ */ jsxRuntime.jsx(
296
- ui.CurrencyInput,
297
- {
298
- label: t2("inventory.productForm.costPrice"),
299
- value: costPrice,
300
- onChange: setCostPrice,
301
- symbol: currency.symbol,
302
- locale: currency.locale,
303
- currencyCode: currency.code
304
- }
305
- ),
306
- /* @__PURE__ */ jsxRuntime.jsx(
307
- ui.CurrencyInput,
308
- {
309
- label: t2("inventory.productForm.salePrice"),
310
- value: salePrice,
311
- onChange: setSalePrice,
312
- symbol: currency.symbol,
313
- locale: currency.locale,
314
- currencyCode: currency.code
315
- }
316
- ),
317
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
318
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.margin") }),
319
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 rounded-lg border bg-muted/20 px-3 py-2 text-sm tabular-nums text-right", children: margin !== null ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: Number(margin) >= 0 ? "text-success" : "text-destructive", children: [
320
- margin,
321
- "%"
322
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "\u2014" }) })
323
- ] })
324
- ] }) }),
325
- /* @__PURE__ */ jsxRuntime.jsx(FormSection, { title: t2("inventory.productForm.stockLevels"), subtitle: t2("inventory.productForm.stockLevelsDesc"), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 sm:grid-cols-2 max-w-md", children: [
326
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
327
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.minQuantity") }),
328
- /* @__PURE__ */ jsxRuntime.jsx(
329
- "input",
330
- {
331
- type: "number",
332
- min: 0,
333
- value: minQuantity,
334
- onChange: (e) => setMinQuantity(Number(e.target.value) || 0),
335
- placeholder: "0",
336
- className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
337
- }
338
- ),
339
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-1", children: t2("inventory.productForm.minQuantityHint") })
340
- ] }),
341
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
342
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.maxQuantity") }),
343
- /* @__PURE__ */ jsxRuntime.jsx(
344
- "input",
345
- {
346
- type: "number",
347
- min: 0,
348
- value: maxQuantity,
349
- onChange: (e) => setMaxQuantity(Number(e.target.value) || 0),
350
- placeholder: t2("inventory.productForm.optional"),
351
- className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
352
- }
353
- ),
354
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground mt-1", children: t2("inventory.productForm.maxQuantityHint") })
355
- ] })
356
- ] }) })
255
+ )
357
256
  ] });
358
257
  }
359
258
  var MOVEMENT_TYPES = [
@@ -441,26 +340,38 @@ function StockMovementView({ defaultType, onSaved, viewMovement }) {
441
340
  const fetchProducts = useInventoryStore((s) => s.fetchProducts);
442
341
  const fetchLocations = useInventoryStore((s) => s.fetchLocations);
443
342
  const createMovement = useInventoryStore((s) => s.createMovement);
444
- const [step, setStep] = React11.useState(1);
445
- const [positions, setPositions] = React11.useState([]);
446
- const [movementType, setMovementType] = React11.useState(defaultType);
447
- const [productId, setProductId] = React11.useState("");
448
- const [productLabel, setProductLabel] = React11.useState("");
449
- const [quantity, setQuantity] = React11.useState(1);
450
- const [unitCost, setUnitCost] = React11.useState(0);
451
- const [locationId, setLocationId] = React11.useState("");
452
- const [destLocationId, setDestLocationId] = React11.useState("");
453
- const [reason, setReason] = React11.useState("");
454
- const [documentNumber, setDocumentNumber] = React11.useState("");
455
- const [notes, setNotes] = React11.useState("");
456
- const [batchNumber, setBatchNumber] = React11.useState("");
457
- const [expirationDate, setExpirationDate] = React11.useState("");
458
- const [saving, setSaving] = React11.useState(false);
459
- const [savedMovement, setSavedMovement] = React11.useState(null);
460
- React11.useEffect(() => {
343
+ const [step, setStep] = React2.useState(1);
344
+ const [positions, setPositions] = React2.useState([]);
345
+ const [movementType, setMovementType] = React2.useState(defaultType);
346
+ const [productId, setProductId] = React2.useState("");
347
+ const [productLabel, setProductLabel] = React2.useState("");
348
+ const [quantity, setQuantity] = React2.useState(1);
349
+ const [unitCost, setUnitCost] = React2.useState(0);
350
+ const [locationId, setLocationId] = React2.useState("");
351
+ const [destLocationId, setDestLocationId] = React2.useState("");
352
+ const [reason, setReason] = React2.useState("");
353
+ const [documentNumber, setDocumentNumber] = React2.useState("");
354
+ const [notes, setNotes] = React2.useState("");
355
+ const [batchNumber, setBatchNumber] = React2.useState("");
356
+ const [expirationDate, setExpirationDate] = React2.useState("");
357
+ const [saving, setSaving] = React2.useState(false);
358
+ const [savedMovement, setSavedMovement] = React2.useState(null);
359
+ React2.useEffect(() => {
461
360
  fetchProducts({});
462
361
  fetchLocations();
463
362
  }, []);
363
+ const positionByLocation = React2.useMemo(() => {
364
+ const map = {};
365
+ for (const pos of positions) {
366
+ if (pos.stockLocationId) {
367
+ map[pos.stockLocationId] = (map[pos.stockLocationId] ?? 0) + pos.quantity;
368
+ }
369
+ }
370
+ return map;
371
+ }, [positions]);
372
+ const sortedLocations = React2.useMemo(() => {
373
+ return [...locations].sort((a, b) => (positionByLocation[b.id] ?? 0) - (positionByLocation[a.id] ?? 0));
374
+ }, [locations, positionByLocation]);
464
375
  if (viewMovement) {
465
376
  return /* @__PURE__ */ jsxRuntime.jsx(MovementDetail, { movement: viewMovement, currency, onClose: () => onSaved?.() });
466
377
  }
@@ -495,18 +406,6 @@ function StockMovementView({ defaultType, onSaved, viewMovement }) {
495
406
  setPositions([]);
496
407
  }
497
408
  }
498
- const positionByLocation = React11.useMemo(() => {
499
- const map = {};
500
- for (const pos of positions) {
501
- if (pos.stockLocationId) {
502
- map[pos.stockLocationId] = (map[pos.stockLocationId] ?? 0) + pos.quantity;
503
- }
504
- }
505
- return map;
506
- }, [positions]);
507
- const sortedLocations = React11.useMemo(() => {
508
- return [...locations].sort((a, b) => (positionByLocation[b.id] ?? 0) - (positionByLocation[a.id] ?? 0));
509
- }, [locations, positionByLocation]);
510
409
  const canProceedStep1 = !!productId;
511
410
  const canProceedStep2 = quantity > 0 && (!needsReason || reason.trim()) && (!needsDest || destLocationId);
512
411
  const title = defaultType === "entry" ? t2("inventory.stock.entry") : defaultType === "exit" ? t2("inventory.stock.exit") : t2("inventory.stock.movement");
@@ -810,11 +709,11 @@ function MovementHistoryView({ onViewDetail } = {}) {
810
709
  const movements = useInventoryStore((s) => s.movements);
811
710
  const movementsLoading = useInventoryStore((s) => s.movementsLoading);
812
711
  const fetchMovements = useInventoryStore((s) => s.fetchMovements);
813
- const [search, setSearch] = React11.useState("");
814
- React11.useEffect(() => {
712
+ const [search, setSearch] = React2.useState("");
713
+ React2.useEffect(() => {
815
714
  fetchMovements({ search: search || void 0 });
816
715
  }, [search]);
817
- const columns = React11.useMemo(() => [
716
+ const columns = React2.useMemo(() => [
818
717
  {
819
718
  accessorKey: "movementDate",
820
719
  header: t2("inventory.history.columnDate"),
@@ -901,7 +800,7 @@ function RecipesView({ onNew, onView }) {
901
800
  const recipes = useInventoryStore((s) => s.recipes);
902
801
  const recipesLoading = useInventoryStore((s) => s.recipesLoading);
903
802
  const fetchRecipes = useInventoryStore((s) => s.fetchRecipes);
904
- React11.useEffect(() => {
803
+ React2.useEffect(() => {
905
804
  fetchRecipes();
906
805
  }, []);
907
806
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
@@ -973,15 +872,15 @@ function RecipeFormView({ onSaved }) {
973
872
  const t2 = core.useTranslation();
974
873
  const provider = useInventoryProvider();
975
874
  const createRecipe = useInventoryStore((s) => s.createRecipe);
976
- const [name, setName] = React11.useState("");
977
- const [description, setDescription] = React11.useState("");
978
- const [productId, setProductId] = React11.useState("");
979
- const [productName, setProductName] = React11.useState("");
980
- const [yieldQuantity, setYieldQuantity] = React11.useState(1);
981
- const [prepTime, setPrepTime] = React11.useState("");
982
- const [instructions, setInstructions] = React11.useState("");
983
- const [ingredients, setIngredients] = React11.useState([]);
984
- const [saving, setSaving] = React11.useState(false);
875
+ const [name, setName] = React2.useState("");
876
+ const [description, setDescription] = React2.useState("");
877
+ const [productId, setProductId] = React2.useState("");
878
+ const [productName, setProductName] = React2.useState("");
879
+ const [yieldQuantity, setYieldQuantity] = React2.useState(1);
880
+ const [prepTime, setPrepTime] = React2.useState("");
881
+ const [instructions, setInstructions] = React2.useState("");
882
+ const [ingredients, setIngredients] = React2.useState([]);
883
+ const [saving, setSaving] = React2.useState(false);
985
884
  function addIngredient() {
986
885
  setIngredients([...ingredients, { _id: nextId(), productId: "", productName: "", quantity: 1, unitId: "", unitName: "", notes: "" }]);
987
886
  }
@@ -1193,10 +1092,10 @@ function DetailSkeleton() {
1193
1092
  function RecipeDetailView({ recipeId, onBack }) {
1194
1093
  const t2 = core.useTranslation();
1195
1094
  const provider = useInventoryProvider();
1196
- const [recipe, setRecipe] = React11.useState(null);
1197
- const [ingredients, setIngredients] = React11.useState([]);
1198
- const [loading, setLoading] = React11.useState(true);
1199
- React11.useEffect(() => {
1095
+ const [recipe, setRecipe] = React2.useState(null);
1096
+ const [ingredients, setIngredients] = React2.useState([]);
1097
+ const [loading, setLoading] = React2.useState(true);
1098
+ React2.useEffect(() => {
1200
1099
  setLoading(true);
1201
1100
  Promise.all([
1202
1101
  provider.getRecipeById(recipeId),
@@ -1355,7 +1254,7 @@ var STEPS = [
1355
1254
  ];
1356
1255
  function InventoryOnboarding({ onComplete }) {
1357
1256
  const t2 = core.useTranslation();
1358
- const [step, setStep] = React11.useState(0);
1257
+ const [step, setStep] = React2.useState(0);
1359
1258
  const current = STEPS[step];
1360
1259
  const isLast = step === STEPS.length - 1;
1361
1260
  const Icon = current.icon;
@@ -1434,7 +1333,7 @@ function InventoryPage({ config, provider, store, registries }) {
1434
1333
  "recipes-detail": 1,
1435
1334
  settings: 1
1436
1335
  }, "dashboard");
1437
- const [onboardingComplete, setOnboardingComplete] = React11.useState(() => {
1336
+ const [onboardingComplete, setOnboardingComplete] = React2.useState(() => {
1438
1337
  try {
1439
1338
  return localStorage.getItem("saas-core:inventory-onboarded") === "true";
1440
1339
  } catch {
@@ -1444,7 +1343,7 @@ function InventoryPage({ config, provider, store, registries }) {
1444
1343
  const isSettings = view === "settings";
1445
1344
  const isSummary = view === "dashboard" || view === "";
1446
1345
  const nav = buildNav(config, view, navigate, t2);
1447
- const quickActions = React11.useMemo(() => {
1346
+ const quickActions = React2.useMemo(() => {
1448
1347
  const actions = [
1449
1348
  {
1450
1349
  id: "new-product",
@@ -1490,8 +1389,8 @@ function InventoryPage({ config, provider, store, registries }) {
1490
1389
  }
1491
1390
  const renderView = saas.createViewRouter([
1492
1391
  { id: "products-list", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
1493
- { id: "products-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductFormView, { onSaved: () => navigate("products-list") }) },
1494
- { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(ProductFormView, { editId: id, onSaved: () => navigate("products-list") }) },
1392
+ { id: "products-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
1393
+ { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) },
1495
1394
  { id: "stock-entry", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
1496
1395
  { id: "stock-exit", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
1497
1396
  { id: "stock-history", render: () => /* @__PURE__ */ jsxRuntime.jsx(MovementHistoryView, { onViewDetail: (id) => navigate(`stock-detail:${id}`) }) },
@@ -1527,7 +1426,7 @@ function InventoryPage({ config, provider, store, registries }) {
1527
1426
  }
1528
1427
  function useEnsureSummary() {
1529
1428
  const fetchSummary = useInventoryStore((s) => s.fetchSummary);
1530
- React11.useEffect(() => {
1429
+ React2.useEffect(() => {
1531
1430
  void fetchSummary();
1532
1431
  }, []);
1533
1432
  }
@@ -1888,6 +1787,7 @@ function createSupabaseInventoryProvider() {
1888
1787
  const { core} = getClients();
1889
1788
  let qb = core.from("products").select("*", { count: "exact" });
1890
1789
  if (query.search) qb = qb.ilike("name", `%${query.search}%`);
1790
+ if (query.productType) qb = qb.eq("metadata->>productType", query.productType);
1891
1791
  if (query.isActive !== void 0) qb = qb.eq("is_active", query.isActive);
1892
1792
  if (query.lowStockOnly) qb = qb.lte("stock", 0);
1893
1793
  const page = query.page ?? 1;
@@ -2856,7 +2756,7 @@ function createInventoryPlugin(options) {
2856
2756
  );
2857
2757
  const store = createInventoryStore(provider);
2858
2758
  const dashboardWidgets = createInventoryDashboardWidgets({ config, provider, store });
2859
- const PageComponent = () => React11__default.default.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
2759
+ const PageComponent = () => React2__default.default.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
2860
2760
  return {
2861
2761
  id: "inventory",
2862
2762
  name: config.labels.pageTitle,
@@ -2919,7 +2819,7 @@ function createInventoryPlugin(options) {
2919
2819
  label: "Inventory",
2920
2820
  icon: "Package",
2921
2821
  component: (() => {
2922
- const Tab = () => React11__default.default.createElement(InventoryGeneralSettings);
2822
+ const Tab = () => React2__default.default.createElement(InventoryGeneralSettings);
2923
2823
  Tab.displayName = "InventorySettingsTab";
2924
2824
  return Tab;
2925
2825
  })(),