@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.js CHANGED
@@ -1,9 +1,9 @@
1
- import React11, { useState, useMemo, useEffect } from 'react';
2
- import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader, ListView, useSaveBar, CurrencyInput, SearchSelect, cn, DatePicker, Breadcrumb, DataTable, DashboardCanvas, KpiCard, Card, CardHeader, CardTitle, CardContent } from '@fayz-ai/ui';
3
- import { createPluginContext, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, ToggleRow, formatCurrency } from '@fayz-ai/saas';
1
+ import React2, { useState, useMemo, useEffect } from 'react';
2
+ import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader, SearchSelect, cn, DatePicker, ListView, useSaveBar, Breadcrumb, DataTable, DashboardCanvas, KpiCard, Card, CardHeader, CardTitle, CardContent } from '@fayz-ai/ui';
3
+ import { createPluginContext, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, ToggleRow, CrudListView, CrudFormPage, formatCurrency } from '@fayz-ai/saas';
4
4
  import { registerTranslations, createSafeDataProvider, useTranslation, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
- import { Package, Ruler, MapPin, Check, ChevronRight, ImagePlus, ArrowDownRight, ArrowUpRight, RefreshCw, ArrowRightLeft, Trash2, Plus, BookOpen, Layers, Clock } from 'lucide-react';
6
+ import { Package, Ruler, MapPin, Check, ChevronRight, ArrowDownRight, ArrowUpRight, RefreshCw, ArrowRightLeft, Trash2, Plus, BookOpen, Layers, Clock } from 'lucide-react';
7
7
  import { createStore as createStore$1 } from 'zustand/vanilla';
8
8
 
9
9
  // src/index.ts
@@ -15,339 +15,238 @@ var useInventoryStore = ctx.useStore;
15
15
  function DashboardView() {
16
16
  return /* @__PURE__ */ jsx(DashboardCanvas, { surface: "plugin-home", domain: "inventory", showHeader: false, className: "space-y-6" });
17
17
  }
18
- var useColumns = (currency, t2) => [
19
- {
20
- accessorKey: "name",
21
- header: t2("inventory.productList.product"),
22
- enableSorting: true,
23
- cell: ({ row }) => /* @__PURE__ */ jsxs("div", { children: [
24
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: row.original.name }),
25
- row.original.sku && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: row.original.sku })
26
- ] })
27
- },
28
- {
29
- accessorKey: "productType",
30
- header: t2("inventory.productList.type"),
31
- cell: ({ getValue }) => /* @__PURE__ */ 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() })
32
- },
33
- {
34
- accessorKey: "currentQuantity",
35
- header: t2("inventory.productList.stock"),
36
- enableSorting: true,
37
- meta: { align: "right" },
38
- cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `text-right block ${row.original.currentQuantity <= row.original.minQuantity ? "text-destructive font-medium" : ""}`, children: row.original.currentQuantity })
39
- },
40
- {
41
- accessorKey: "costPrice",
42
- header: t2("inventory.productList.cost"),
43
- meta: { align: "right" },
44
- cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-muted-foreground", children: formatCurrency(getValue(), currency) })
45
- },
46
- {
47
- id: "value",
48
- header: t2("inventory.productList.value"),
49
- meta: { align: "right" },
50
- cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block font-medium", children: formatCurrency(row.original.currentQuantity * row.original.costPrice, currency) })
51
- }
52
- ];
18
+ var TYPE_DESCRIPTION_KEYS = {
19
+ ingredient: "inventory.productForm.typeIngredient",
20
+ sale: "inventory.productForm.typeSale",
21
+ intermediate: "inventory.productForm.typeIntermediate",
22
+ asset: "inventory.productForm.typeAsset"
23
+ };
24
+ function buildProductEntity(t2, productTypes, currency) {
25
+ const typeOptions = productTypes.map((pt) => ({
26
+ label: pt.label,
27
+ value: pt.value,
28
+ description: TYPE_DESCRIPTION_KEYS[pt.value] ? t2(TYPE_DESCRIPTION_KEYS[pt.value]) : void 0
29
+ }));
30
+ const typeLabel = (v) => productTypes.find((p) => p.value === v)?.label ?? String(v ?? "");
31
+ const fields = [
32
+ // — General Information (form) + Product column (table) —
33
+ {
34
+ key: "name",
35
+ label: t2("inventory.productForm.name"),
36
+ type: "text",
37
+ required: true,
38
+ group: "general",
39
+ placeholder: t2("inventory.productForm.namePlaceholder"),
40
+ searchable: true,
41
+ showInTable: true,
42
+ sortable: true,
43
+ renderCell: (_v, row) => /* @__PURE__ */ jsxs("div", { children: [
44
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: row.name }),
45
+ row.sku && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: row.sku })
46
+ ] })
47
+ },
48
+ { key: "brand", label: t2("inventory.productForm.brand"), type: "text", group: "general", placeholder: t2("inventory.productForm.brandPlaceholder"), showInTable: false },
49
+ { key: "sku", label: t2("inventory.productForm.sku"), type: "text", group: "general", placeholder: t2("inventory.productForm.skuPlaceholder"), searchable: true, showInTable: false },
50
+ { key: "barcode", label: t2("inventory.productForm.barcode"), type: "text", group: "general", placeholder: t2("inventory.productForm.barcodePlaceholder"), searchable: true, showInTable: false },
51
+ { key: "description", label: t2("inventory.productForm.description"), type: "textarea", group: "general", span: 2, placeholder: t2("inventory.productForm.descriptionPlaceholder"), showInTable: false },
52
+ // — Classification (form: segmented) + Type column (table: badge) —
53
+ {
54
+ key: "productType",
55
+ label: t2("inventory.productForm.classification"),
56
+ type: "segmented",
57
+ group: "classification",
58
+ span: 2,
59
+ options: typeOptions,
60
+ showInTable: true,
61
+ sortable: false,
62
+ renderCell: (v) => /* @__PURE__ */ 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) })
63
+ },
64
+ // — Stock column (list only) — placed before Cost for the desired column order
65
+ {
66
+ key: "currentQuantity",
67
+ label: t2("inventory.productList.stock"),
68
+ type: "number",
69
+ showInForm: false,
70
+ showInTable: true,
71
+ sortable: true,
72
+ renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: `text-right block ${row.currentQuantity <= row.minQuantity ? "text-destructive font-medium" : ""}`, children: row.currentQuantity })
73
+ },
74
+ // — Pricing (form) + Cost column (table) —
75
+ {
76
+ key: "costPrice",
77
+ label: t2("inventory.productForm.costPrice"),
78
+ type: "currency",
79
+ group: "pricing",
80
+ currency: currency.code,
81
+ currencySymbol: currency.symbol,
82
+ currencyLocale: currency.locale,
83
+ showInTable: true,
84
+ sortable: false,
85
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "text-right block text-muted-foreground", children: formatCurrency(Number(v) || 0, currency) })
86
+ },
87
+ // — Value column (list only): stock × cost —
88
+ {
89
+ key: "value",
90
+ label: t2("inventory.productList.value"),
91
+ type: "number",
92
+ showInForm: false,
93
+ showInTable: true,
94
+ sortable: false,
95
+ renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: "text-right block font-medium", children: formatCurrency((row.currentQuantity || 0) * (row.costPrice || 0), currency) })
96
+ },
97
+ { key: "salePrice", label: t2("inventory.productForm.salePrice"), type: "currency", group: "pricing", currency: currency.code, currencySymbol: currency.symbol, currencyLocale: currency.locale, showInTable: false },
98
+ {
99
+ key: "margin",
100
+ label: t2("inventory.productForm.margin"),
101
+ type: "computed",
102
+ group: "pricing",
103
+ showInTable: false,
104
+ compute: (vals) => {
105
+ const cost = Number(vals.costPrice) || 0;
106
+ const sale = Number(vals.salePrice) || 0;
107
+ if (sale <= 0 || cost <= 0) return null;
108
+ const m = (sale - cost) / cost * 100;
109
+ return { display: `${m.toFixed(1)}%`, tone: m >= 0 ? "positive" : "negative" };
110
+ }
111
+ },
112
+ // — Stock Levels (form only) —
113
+ { key: "minQuantity", label: t2("inventory.productForm.minQuantity"), type: "number", group: "stock", min: 0, placeholder: "0", hint: t2("inventory.productForm.minQuantityHint"), showInTable: false },
114
+ { 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 }
115
+ ];
116
+ return {
117
+ name: t2("inventory.stock.product"),
118
+ namePlural: t2("inventory.nav.products"),
119
+ icon: "Package",
120
+ displayField: "name",
121
+ fields,
122
+ fieldGroups: [
123
+ { id: "general", label: t2("inventory.productForm.generalInfo"), columns: 2, imageSlot: true },
124
+ { id: "classification", label: t2("inventory.productForm.classification"), description: t2("inventory.productForm.classificationDesc"), columns: 1 },
125
+ { id: "pricing", label: t2("inventory.productForm.pricing"), columns: 3 },
126
+ { id: "stock", label: t2("inventory.productForm.stockLevels"), description: t2("inventory.productForm.stockLevelsDesc"), columns: 2 }
127
+ ],
128
+ facets: [{ field: "productType", allLabel: t2("crud.list.allFacet") }]
129
+ };
130
+ }
53
131
  function ProductListView({ onNew, onEdit }) {
54
132
  const t2 = useTranslation();
55
133
  const { currency, productTypes } = useInventoryConfig();
56
134
  const products = useInventoryStore((s) => s.products);
57
135
  const productsTotal = useInventoryStore((s) => s.productsTotal);
58
- const productsLoading = useInventoryStore((s) => s.productsLoading);
136
+ useInventoryStore((s) => s.productsLoading);
59
137
  const fetchProducts = useInventoryStore((s) => s.fetchProducts);
60
138
  const [search, setSearch] = useState("");
61
139
  const [typeFilter, setTypeFilter] = useState();
140
+ const [loadedOnce, setLoadedOnce] = useState(false);
62
141
  useEffect(() => {
63
- fetchProducts({ productType: typeFilter, search: search || void 0 });
142
+ let active = true;
143
+ Promise.resolve(fetchProducts({ productType: typeFilter, search: search || void 0 })).finally(() => {
144
+ if (active) setLoadedOnce(true);
145
+ });
146
+ return () => {
147
+ active = false;
148
+ };
64
149
  }, [typeFilter, search]);
65
- const columns = useColumns(currency, t2);
66
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
67
- /* @__PURE__ */ jsx(SubpageHeader, { title: t2("inventory.productList.title"), subtitle: t2("inventory.productList.subtitle", { count: String(productsTotal) }) }),
68
- /* @__PURE__ */ jsx(
69
- ListView,
70
- {
71
- columns,
72
- data: products,
73
- loading: productsLoading,
74
- searchPlaceholder: t2("inventory.productList.searchPlaceholder"),
75
- search,
76
- onSearchChange: setSearch,
77
- searchDebounce: 0,
78
- tags: productTypes.map((t3) => ({ value: t3.value, label: t3.label })),
79
- activeTag: typeFilter,
80
- onTagChange: setTypeFilter,
81
- newLabel: t2("inventory.productList.newProduct"),
82
- onNew,
83
- onRowClick: (row) => onEdit?.(row.id),
84
- emptyIcon: Package,
85
- emptyMessage: t2("inventory.productList.empty"),
86
- emptyActionLabel: onNew ? t2("inventory.productList.createFirst") : void 0,
87
- onEmptyAction: onNew
88
- }
89
- )
90
- ] });
91
- }
92
- var TYPE_DESCRIPTION_KEYS = {
93
- ingredient: "inventory.productForm.typeIngredient",
94
- sale: "inventory.productForm.typeSale",
95
- intermediate: "inventory.productForm.typeIntermediate",
96
- asset: "inventory.productForm.typeAsset"
97
- };
98
- function FormSection({ title, subtitle, children }) {
99
- return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card shadow-sm", children: [
100
- /* @__PURE__ */ jsxs("div", { className: "px-5 py-3 border-b", children: [
101
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: title }),
102
- subtitle && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5", children: subtitle })
103
- ] }),
104
- /* @__PURE__ */ jsx("div", { className: "p-5 space-y-4", children })
105
- ] });
150
+ const entity = useMemo(() => buildProductEntity(t2, productTypes, currency), [t2, productTypes, currency]);
151
+ const facets = useMemo(() => [{
152
+ field: "productType",
153
+ allLabel: t2("crud.list.allFacet"),
154
+ options: productTypes.map((p) => ({ value: p.value, label: p.label }))
155
+ }], [productTypes, t2]);
156
+ return /* @__PURE__ */ jsx(
157
+ CrudListView,
158
+ {
159
+ entityDef: entity,
160
+ items: loadedOnce ? products : null,
161
+ total: productsTotal,
162
+ search,
163
+ onSearchChange: setSearch,
164
+ searchPlaceholder: t2("inventory.productList.searchPlaceholder"),
165
+ facets,
166
+ activeFilters: { productType: typeFilter },
167
+ onFacetChange: (_field, value) => setTypeFilter(value),
168
+ onNew,
169
+ addLabel: t2("inventory.productList.newProduct"),
170
+ onRowClick: (row) => onEdit?.(row.id)
171
+ }
172
+ );
106
173
  }
107
- function ProductFormView({ editId, onSaved }) {
174
+ function ProductCrudForm({ editId, onSaved }) {
108
175
  const t2 = useTranslation();
109
176
  const { productTypes, currency } = useInventoryConfig();
110
177
  const provider = useInventoryProvider();
111
178
  const createProduct = useInventoryStore((s) => s.createProduct);
112
179
  const isEdit = !!editId;
113
- const [loaded, setLoaded] = useState(false);
114
- const [name, setName] = useState("");
115
- const [sku, setSku] = useState("");
116
- const [barcode, setBarcode] = useState("");
117
- const [brand, setBrand] = useState("");
118
- const [productType, setProductType] = useState(productTypes[0]?.value ?? "ingredient");
119
- const [costPrice, setCostPrice] = useState(0);
120
- const [salePrice, setSalePrice] = useState(0);
121
- const [minQuantity, setMinQuantity] = useState(0);
122
- const [maxQuantity, setMaxQuantity] = useState(0);
123
- const [description, setDescription] = useState("");
124
- const [saving, setSaving] = useState(false);
125
- useEffect(() => {
126
- if (!editId || loaded) return;
180
+ const [initialData, setInitialData] = React2.useState(isEdit ? null : {});
181
+ const [headerName, setHeaderName] = React2.useState("");
182
+ React2.useEffect(() => {
183
+ if (!editId) return;
184
+ let cancelled = false;
127
185
  (async () => {
128
- const product = await provider.getProductById(editId);
129
- if (!product) return;
130
- setName(product.name);
131
- setSku(product.sku ?? "");
132
- setBarcode(product.barcode ?? "");
133
- setBrand(product.brand ?? "");
134
- setProductType(product.productType);
135
- setCostPrice(product.costPrice);
136
- setSalePrice(product.salePrice ?? 0);
137
- setMinQuantity(product.minQuantity);
138
- setMaxQuantity(product.maxQuantity ?? 0);
139
- setDescription(product.description ?? "");
140
- setLoaded(true);
186
+ const p = await provider.getProductById(editId);
187
+ if (cancelled) return;
188
+ setHeaderName(p?.name ?? "");
189
+ setInitialData(
190
+ p ? {
191
+ name: p.name,
192
+ brand: p.brand ?? "",
193
+ sku: p.sku ?? "",
194
+ barcode: p.barcode ?? "",
195
+ description: p.description ?? "",
196
+ productType: p.productType,
197
+ costPrice: p.costPrice,
198
+ salePrice: p.salePrice ?? 0,
199
+ minQuantity: p.minQuantity,
200
+ maxQuantity: p.maxQuantity ?? 0
201
+ } : {}
202
+ );
141
203
  })();
204
+ return () => {
205
+ cancelled = true;
206
+ };
142
207
  }, [editId]);
143
- const fields = { name, sku, barcode, brand, productType, costPrice, salePrice, minQuantity, maxQuantity, description };
144
- const ready = !editId || loaded;
145
- const snapshot = React11.useRef(null);
146
- useEffect(() => {
147
- if (ready && snapshot.current === null) snapshot.current = JSON.stringify(fields);
148
- }, [ready]);
149
- const dirty = snapshot.current !== null && JSON.stringify(fields) !== snapshot.current;
150
- useSaveBar({
151
- dirty,
152
- saving,
153
- onSave: () => {
154
- void handleSave();
155
- },
156
- onDiscard: () => onSaved?.(),
157
- saveLabel: t2("inventory.productForm.save")
158
- });
159
- async function handleSave() {
160
- if (!name.trim()) {
208
+ const entity = React2.useMemo(() => buildProductEntity(t2, productTypes, currency), [t2, productTypes, currency]);
209
+ async function save(values) {
210
+ const name = String(values.name ?? "").trim();
211
+ if (!name) {
161
212
  toast.error(t2("common.formIncomplete"));
162
- return;
213
+ throw new Error("name required");
163
214
  }
164
- setSaving(true);
165
- try {
166
- if (isEdit && editId) {
167
- await provider.updateProduct(editId, {
168
- name,
169
- sku: sku || void 0,
170
- barcode: barcode || void 0,
171
- brand: brand || void 0,
172
- productType,
173
- costPrice,
174
- salePrice: salePrice || void 0,
175
- minQuantity,
176
- maxQuantity: maxQuantity || void 0,
177
- description: description || void 0
178
- });
179
- } else {
180
- await createProduct({
181
- name,
182
- sku: sku || void 0,
183
- barcode: barcode || void 0,
184
- brand: brand || void 0,
185
- productType,
186
- costPrice,
187
- salePrice: salePrice || void 0,
188
- minQuantity,
189
- maxQuantity: maxQuantity || void 0,
190
- description: description || void 0
191
- });
192
- }
193
- onSaved?.();
194
- } finally {
195
- setSaving(false);
215
+ const payload = {
216
+ name,
217
+ sku: values.sku || void 0,
218
+ barcode: values.barcode || void 0,
219
+ brand: values.brand || void 0,
220
+ productType: values.productType ?? productTypes[0]?.value,
221
+ costPrice: Number(values.costPrice) || 0,
222
+ salePrice: Number(values.salePrice) || void 0,
223
+ minQuantity: Number(values.minQuantity) || 0,
224
+ maxQuantity: Number(values.maxQuantity) || void 0,
225
+ description: values.description || void 0
226
+ };
227
+ if (isEdit && editId) {
228
+ await provider.updateProduct(editId, payload);
229
+ } else {
230
+ await createProduct(payload);
196
231
  }
232
+ onSaved?.();
197
233
  }
198
- const title = isEdit ? name || t2("inventory.productForm.editProduct") : t2("inventory.productForm.newProduct");
234
+ const title = isEdit ? headerName || t2("inventory.productForm.editProduct") : t2("inventory.productForm.newProduct");
199
235
  const subtitle = isEdit ? void 0 : t2("inventory.productForm.addToCatalog");
200
- const margin = salePrice > 0 && costPrice > 0 ? ((salePrice - costPrice) / costPrice * 100).toFixed(1) : null;
201
236
  return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
202
- /* @__PURE__ */ jsx(
203
- SubpageHeader,
237
+ /* @__PURE__ */ jsx(SubpageHeader, { title, subtitle, onBack: onSaved, parentLabel: t2("inventory.nav.products") }),
238
+ initialData && /* @__PURE__ */ jsx(
239
+ CrudFormPage,
204
240
  {
205
- title,
206
- subtitle,
207
- onBack: onSaved,
208
- parentLabel: t2("inventory.nav.products")
241
+ entityDef: entity,
242
+ mode: isEdit ? "edit" : "create",
243
+ initialData,
244
+ onSubmit: save,
245
+ onCancel: () => onSaved?.(),
246
+ namePlural: t2("inventory.nav.products"),
247
+ hideBreadcrumb: true
209
248
  }
210
- ),
211
- /* @__PURE__ */ jsxs(FormSection, { title: t2("inventory.productForm.generalInfo"), children: [
212
- /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
213
- /* @__PURE__ */ jsx("div", { className: "shrink-0", children: /* @__PURE__ */ 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__ */ jsx(ImagePlus, { className: "h-5 w-5" }) }) }),
214
- /* @__PURE__ */ jsxs("div", { className: "flex-1 grid gap-3 sm:grid-cols-2", children: [
215
- /* @__PURE__ */ jsxs("div", { className: "sm:col-span-1", children: [
216
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium text-muted-foreground", children: [
217
- t2("inventory.productForm.name"),
218
- " *"
219
- ] }),
220
- /* @__PURE__ */ jsx(
221
- "input",
222
- {
223
- type: "text",
224
- value: name,
225
- onChange: (e) => setName(e.target.value),
226
- placeholder: t2("inventory.productForm.namePlaceholder"),
227
- autoFocus: true,
228
- 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"
229
- }
230
- )
231
- ] }),
232
- /* @__PURE__ */ jsxs("div", { children: [
233
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.brand") }),
234
- /* @__PURE__ */ jsx(
235
- "input",
236
- {
237
- type: "text",
238
- value: brand,
239
- onChange: (e) => setBrand(e.target.value),
240
- placeholder: t2("inventory.productForm.brandPlaceholder"),
241
- 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"
242
- }
243
- )
244
- ] })
245
- ] })
246
- ] }),
247
- /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
248
- /* @__PURE__ */ jsxs("div", { children: [
249
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.sku") }),
250
- /* @__PURE__ */ 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" })
251
- ] }),
252
- /* @__PURE__ */ jsxs("div", { children: [
253
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.barcode") }),
254
- /* @__PURE__ */ 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" })
255
- ] })
256
- ] }),
257
- /* @__PURE__ */ jsxs("div", { children: [
258
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.description") }),
259
- /* @__PURE__ */ jsx(
260
- "textarea",
261
- {
262
- value: description,
263
- onChange: (e) => setDescription(e.target.value),
264
- rows: 2,
265
- placeholder: t2("inventory.productForm.descriptionPlaceholder"),
266
- 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"
267
- }
268
- )
269
- ] })
270
- ] }),
271
- /* @__PURE__ */ jsx(FormSection, { title: t2("inventory.productForm.classification"), subtitle: t2("inventory.productForm.classificationDesc"), children: /* @__PURE__ */ jsx("div", { className: "grid gap-2 sm:grid-cols-2 lg:grid-cols-4", children: productTypes.map((pt) => {
272
- const active = productType === pt.value;
273
- const descKey = TYPE_DESCRIPTION_KEYS[pt.value];
274
- const desc = descKey ? t2(descKey) : "";
275
- return /* @__PURE__ */ jsxs(
276
- "button",
277
- {
278
- onClick: () => setProductType(pt.value),
279
- 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"}`,
280
- children: [
281
- /* @__PURE__ */ jsx("p", { className: `text-sm font-medium ${active ? "text-primary" : ""}`, children: pt.label }),
282
- desc && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5 leading-tight", children: desc })
283
- ]
284
- },
285
- pt.value
286
- );
287
- }) }) }),
288
- /* @__PURE__ */ jsx(FormSection, { title: t2("inventory.productForm.pricing"), children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [
289
- /* @__PURE__ */ jsx(
290
- CurrencyInput,
291
- {
292
- label: t2("inventory.productForm.costPrice"),
293
- value: costPrice,
294
- onChange: setCostPrice,
295
- symbol: currency.symbol,
296
- locale: currency.locale,
297
- currencyCode: currency.code
298
- }
299
- ),
300
- /* @__PURE__ */ jsx(
301
- CurrencyInput,
302
- {
303
- label: t2("inventory.productForm.salePrice"),
304
- value: salePrice,
305
- onChange: setSalePrice,
306
- symbol: currency.symbol,
307
- locale: currency.locale,
308
- currencyCode: currency.code
309
- }
310
- ),
311
- /* @__PURE__ */ jsxs("div", { children: [
312
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.margin") }),
313
- /* @__PURE__ */ 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__ */ jsxs("span", { className: Number(margin) >= 0 ? "text-success" : "text-destructive", children: [
314
- margin,
315
- "%"
316
- ] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) })
317
- ] })
318
- ] }) }),
319
- /* @__PURE__ */ jsx(FormSection, { title: t2("inventory.productForm.stockLevels"), subtitle: t2("inventory.productForm.stockLevelsDesc"), children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2 max-w-md", children: [
320
- /* @__PURE__ */ jsxs("div", { children: [
321
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.minQuantity") }),
322
- /* @__PURE__ */ jsx(
323
- "input",
324
- {
325
- type: "number",
326
- min: 0,
327
- value: minQuantity,
328
- onChange: (e) => setMinQuantity(Number(e.target.value) || 0),
329
- placeholder: "0",
330
- 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"
331
- }
332
- ),
333
- /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-1", children: t2("inventory.productForm.minQuantityHint") })
334
- ] }),
335
- /* @__PURE__ */ jsxs("div", { children: [
336
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.productForm.maxQuantity") }),
337
- /* @__PURE__ */ jsx(
338
- "input",
339
- {
340
- type: "number",
341
- min: 0,
342
- value: maxQuantity,
343
- onChange: (e) => setMaxQuantity(Number(e.target.value) || 0),
344
- placeholder: t2("inventory.productForm.optional"),
345
- 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"
346
- }
347
- ),
348
- /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-1", children: t2("inventory.productForm.maxQuantityHint") })
349
- ] })
350
- ] }) })
249
+ )
351
250
  ] });
352
251
  }
353
252
  var MOVEMENT_TYPES = [
@@ -455,6 +354,18 @@ function StockMovementView({ defaultType, onSaved, viewMovement }) {
455
354
  fetchProducts({});
456
355
  fetchLocations();
457
356
  }, []);
357
+ const positionByLocation = useMemo(() => {
358
+ const map = {};
359
+ for (const pos of positions) {
360
+ if (pos.stockLocationId) {
361
+ map[pos.stockLocationId] = (map[pos.stockLocationId] ?? 0) + pos.quantity;
362
+ }
363
+ }
364
+ return map;
365
+ }, [positions]);
366
+ const sortedLocations = useMemo(() => {
367
+ return [...locations].sort((a, b) => (positionByLocation[b.id] ?? 0) - (positionByLocation[a.id] ?? 0));
368
+ }, [locations, positionByLocation]);
458
369
  if (viewMovement) {
459
370
  return /* @__PURE__ */ jsx(MovementDetail, { movement: viewMovement, currency, onClose: () => onSaved?.() });
460
371
  }
@@ -489,18 +400,6 @@ function StockMovementView({ defaultType, onSaved, viewMovement }) {
489
400
  setPositions([]);
490
401
  }
491
402
  }
492
- const positionByLocation = useMemo(() => {
493
- const map = {};
494
- for (const pos of positions) {
495
- if (pos.stockLocationId) {
496
- map[pos.stockLocationId] = (map[pos.stockLocationId] ?? 0) + pos.quantity;
497
- }
498
- }
499
- return map;
500
- }, [positions]);
501
- const sortedLocations = useMemo(() => {
502
- return [...locations].sort((a, b) => (positionByLocation[b.id] ?? 0) - (positionByLocation[a.id] ?? 0));
503
- }, [locations, positionByLocation]);
504
403
  const canProceedStep1 = !!productId;
505
404
  const canProceedStep2 = quantity > 0 && (!needsReason || reason.trim()) && (!needsDest || destLocationId);
506
405
  const title = defaultType === "entry" ? t2("inventory.stock.entry") : defaultType === "exit" ? t2("inventory.stock.exit") : t2("inventory.stock.movement");
@@ -1484,8 +1383,8 @@ function InventoryPage({ config, provider, store, registries }) {
1484
1383
  }
1485
1384
  const renderView = createViewRouter([
1486
1385
  { id: "products-list", render: () => /* @__PURE__ */ jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
1487
- { id: "products-new", render: () => /* @__PURE__ */ jsx(ProductFormView, { onSaved: () => navigate("products-list") }) },
1488
- { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductFormView, { editId: id, onSaved: () => navigate("products-list") }) },
1386
+ { id: "products-new", render: () => /* @__PURE__ */ jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
1387
+ { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) },
1489
1388
  { id: "stock-entry", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
1490
1389
  { id: "stock-exit", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
1491
1390
  { id: "stock-history", render: () => /* @__PURE__ */ jsx(MovementHistoryView, { onViewDetail: (id) => navigate(`stock-detail:${id}`) }) },
@@ -1882,6 +1781,7 @@ function createSupabaseInventoryProvider() {
1882
1781
  const { core} = getClients();
1883
1782
  let qb = core.from("products").select("*", { count: "exact" });
1884
1783
  if (query.search) qb = qb.ilike("name", `%${query.search}%`);
1784
+ if (query.productType) qb = qb.eq("metadata->>productType", query.productType);
1885
1785
  if (query.isActive !== void 0) qb = qb.eq("is_active", query.isActive);
1886
1786
  if (query.lowStockOnly) qb = qb.lte("stock", 0);
1887
1787
  const page = query.page ?? 1;
@@ -2850,7 +2750,7 @@ function createInventoryPlugin(options) {
2850
2750
  );
2851
2751
  const store = createInventoryStore(provider);
2852
2752
  const dashboardWidgets = createInventoryDashboardWidgets({ config, provider, store });
2853
- const PageComponent = () => React11.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
2753
+ const PageComponent = () => React2.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
2854
2754
  return {
2855
2755
  id: "inventory",
2856
2756
  name: config.labels.pageTitle,
@@ -2913,7 +2813,7 @@ function createInventoryPlugin(options) {
2913
2813
  label: "Inventory",
2914
2814
  icon: "Package",
2915
2815
  component: (() => {
2916
- const Tab = () => React11.createElement(InventoryGeneralSettings);
2816
+ const Tab = () => React2.createElement(InventoryGeneralSettings);
2917
2817
  Tab.displayName = "InventorySettingsTab";
2918
2818
  return Tab;
2919
2819
  })(),