@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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.cjs +265 -365
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +226 -326
- package/dist/index.js.map +1 -1
- package/dist/views/ProductListView.d.ts.map +1 -1
- package/dist/views/StockMovementView.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/InventoryPage.tsx +3 -3
- package/src/data/supabase.ts +2 -0
- package/src/views/ProductListView.tsx +34 -84
- package/src/views/StockMovementView.tsx +18 -16
- package/dist/views/ProductFormView.d.ts +0 -6
- package/dist/views/ProductFormView.d.ts.map +0 -1
- package/src/views/ProductFormView.tsx +0 -283
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader,
|
|
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,
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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 [
|
|
114
|
-
const [
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
213
|
+
throw new Error("name required");
|
|
163
214
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 ?
|
|
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
|
-
|
|
237
|
+
/* @__PURE__ */ jsx(SubpageHeader, { title, subtitle, onBack: onSaved, parentLabel: t2("inventory.nav.products") }),
|
|
238
|
+
initialData && /* @__PURE__ */ jsx(
|
|
239
|
+
CrudFormPage,
|
|
204
240
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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(
|
|
1488
|
-
{ id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(
|
|
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 = () =>
|
|
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 = () =>
|
|
2816
|
+
const Tab = () => React2.createElement(InventoryGeneralSettings);
|
|
2917
2817
|
Tab.displayName = "InventorySettingsTab";
|
|
2918
2818
|
return Tab;
|
|
2919
2819
|
})(),
|