@fayz-ai/plugin-inventory 0.1.1
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/InventoryContext.d.ts +37 -0
- package/dist/InventoryContext.d.ts.map +1 -0
- package/dist/InventoryPage.d.ts +13 -0
- package/dist/InventoryPage.d.ts.map +1 -0
- package/dist/components/InventoryGeneralSettings.d.ts +3 -0
- package/dist/components/InventoryGeneralSettings.d.ts.map +1 -0
- package/dist/components/InventoryOnboarding.d.ts +5 -0
- package/dist/components/InventoryOnboarding.d.ts.map +1 -0
- package/dist/components/InventorySettings.d.ts +8 -0
- package/dist/components/InventorySettings.d.ts.map +1 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +22 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +2936 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2930 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +28 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/DashboardView.d.ts +8 -0
- package/dist/views/DashboardView.d.ts.map +1 -0
- package/dist/views/MovementHistoryView.d.ts +5 -0
- package/dist/views/MovementHistoryView.d.ts.map +1 -0
- package/dist/views/ProductCrudForm.d.ts +6 -0
- package/dist/views/ProductCrudForm.d.ts.map +1 -0
- package/dist/views/ProductFormView.d.ts +6 -0
- package/dist/views/ProductFormView.d.ts.map +1 -0
- package/dist/views/ProductListView.d.ts +6 -0
- package/dist/views/ProductListView.d.ts.map +1 -0
- package/dist/views/RecipeDetailView.d.ts +6 -0
- package/dist/views/RecipeDetailView.d.ts.map +1 -0
- package/dist/views/RecipeFormView.d.ts +5 -0
- package/dist/views/RecipeFormView.d.ts.map +1 -0
- package/dist/views/RecipesView.d.ts +6 -0
- package/dist/views/RecipesView.d.ts.map +1 -0
- package/dist/views/StockMovementView.d.ts +8 -0
- package/dist/views/StockMovementView.d.ts.map +1 -0
- package/dist/views/dashboardWidgets.d.ts +11 -0
- package/dist/views/dashboardWidgets.d.ts.map +1 -0
- package/dist/views/productEntity.d.ts +6 -0
- package/dist/views/productEntity.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/InventoryContext.tsx +40 -0
- package/src/InventoryPage.tsx +170 -0
- package/src/README.md +177 -0
- package/src/components/InventoryGeneralSettings.tsx +26 -0
- package/src/components/InventoryOnboarding.tsx +60 -0
- package/src/components/InventorySettings.tsx +27 -0
- package/src/data/index.ts +2 -0
- package/src/data/mock.ts +266 -0
- package/src/data/supabase.ts +358 -0
- package/src/data/types.ts +35 -0
- package/src/index.ts +191 -0
- package/src/lib/tenant.ts +4 -0
- package/src/locales/en.ts +242 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +242 -0
- package/src/migrations/001_inventory_base.sql +69 -0
- package/src/migrations/002_recipes.sql +34 -0
- package/src/migrations/003_measurement_units.sql +13 -0
- package/src/registries.ts +111 -0
- package/src/store.ts +127 -0
- package/src/types.ts +256 -0
- package/src/views/DashboardView.tsx +11 -0
- package/src/views/MovementHistoryView.tsx +104 -0
- package/src/views/ProductCrudForm.tsx +99 -0
- package/src/views/ProductFormView.tsx +283 -0
- package/src/views/ProductListView.tsx +107 -0
- package/src/views/RecipeDetailView.tsx +192 -0
- package/src/views/RecipeFormView.tsx +235 -0
- package/src/views/RecipesView.tsx +103 -0
- package/src/views/StockMovementView.tsx +516 -0
- package/src/views/dashboardWidgets.tsx +101 -0
- package/src/views/productEntity.tsx +124 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2936 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React11 = require('react');
|
|
4
|
+
var ui = require('@fayz-ai/ui');
|
|
5
|
+
var saas = require('@fayz-ai/saas');
|
|
6
|
+
var core = require('@fayz-ai/core');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
var lucideReact = require('lucide-react');
|
|
9
|
+
var vanilla = require('zustand/vanilla');
|
|
10
|
+
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var React11__default = /*#__PURE__*/_interopDefault(React11);
|
|
14
|
+
|
|
15
|
+
// src/index.ts
|
|
16
|
+
var ctx = saas.createPluginContext("InventoryPage");
|
|
17
|
+
var InventoryContextProvider = ctx.ContextProvider;
|
|
18
|
+
var useInventoryConfig = ctx.useConfig;
|
|
19
|
+
var useInventoryProvider = ctx.useProvider;
|
|
20
|
+
var useInventoryStore = ctx.useStore;
|
|
21
|
+
function DashboardView() {
|
|
22
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.DashboardCanvas, { surface: "plugin-home", domain: "inventory", showHeader: false, className: "space-y-6" });
|
|
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
|
+
];
|
|
59
|
+
function ProductListView({ onNew, onEdit }) {
|
|
60
|
+
const t2 = core.useTranslation();
|
|
61
|
+
const { currency, productTypes } = useInventoryConfig();
|
|
62
|
+
const products = useInventoryStore((s) => s.products);
|
|
63
|
+
const productsTotal = useInventoryStore((s) => s.productsTotal);
|
|
64
|
+
const productsLoading = useInventoryStore((s) => s.productsLoading);
|
|
65
|
+
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 });
|
|
70
|
+
}, [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
|
+
] });
|
|
112
|
+
}
|
|
113
|
+
function ProductFormView({ editId, onSaved }) {
|
|
114
|
+
const t2 = core.useTranslation();
|
|
115
|
+
const { productTypes, currency } = useInventoryConfig();
|
|
116
|
+
const provider = useInventoryProvider();
|
|
117
|
+
const createProduct = useInventoryStore((s) => s.createProduct);
|
|
118
|
+
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;
|
|
133
|
+
(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);
|
|
147
|
+
})();
|
|
148
|
+
}, [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()) {
|
|
167
|
+
ui.toast.error(t2("common.formIncomplete"));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
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);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const title = isEdit ? name || t2("inventory.productForm.editProduct") : t2("inventory.productForm.newProduct");
|
|
205
|
+
const subtitle = isEdit ? void 0 : t2("inventory.productForm.addToCatalog");
|
|
206
|
+
const margin = salePrice > 0 && costPrice > 0 ? ((salePrice - costPrice) / costPrice * 100).toFixed(1) : null;
|
|
207
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
208
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
209
|
+
ui.SubpageHeader,
|
|
210
|
+
{
|
|
211
|
+
title,
|
|
212
|
+
subtitle,
|
|
213
|
+
onBack: onSaved,
|
|
214
|
+
parentLabel: t2("inventory.nav.products")
|
|
215
|
+
}
|
|
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
|
+
] }) })
|
|
357
|
+
] });
|
|
358
|
+
}
|
|
359
|
+
var MOVEMENT_TYPES = [
|
|
360
|
+
{ value: "entry", labelKey: "inventory.stock.entryLabel", icon: lucideReact.ArrowDownRight, color: "text-success", activeColor: "border-success bg-success/10 text-success dark:bg-success/10", descKey: "inventory.stock.entryDesc" },
|
|
361
|
+
{ value: "exit", labelKey: "inventory.stock.exitLabel", icon: lucideReact.ArrowUpRight, color: "text-destructive", activeColor: "border-destructive bg-destructive/10 text-destructive dark:bg-destructive/10", descKey: "inventory.stock.exitDesc" },
|
|
362
|
+
{ value: "adjustment", labelKey: "inventory.stock.adjustmentLabel", icon: lucideReact.RefreshCw, color: "text-info", activeColor: "border-info bg-info/10 text-info dark:bg-info/10", descKey: "inventory.stock.adjustmentDesc" },
|
|
363
|
+
{ value: "transfer", labelKey: "inventory.stock.transferLabel", icon: lucideReact.ArrowRightLeft, color: "text-magic", activeColor: "border-magic bg-magic/10 text-magic dark:bg-magic/10", descKey: "inventory.stock.transferDesc" },
|
|
364
|
+
{ value: "loss", labelKey: "inventory.stock.lossLabel", icon: lucideReact.Trash2, color: "text-warning", activeColor: "border-warning bg-warning/10 text-warning dark:bg-warning/10", descKey: "inventory.stock.lossDesc" }
|
|
365
|
+
];
|
|
366
|
+
function MovementDetail({ movement, currency, onClose }) {
|
|
367
|
+
const t2 = core.useTranslation();
|
|
368
|
+
const typeConfig = MOVEMENT_TYPES.find((mt) => mt.value === movement.movementType);
|
|
369
|
+
const TypeIcon = typeConfig?.icon ?? lucideReact.Package;
|
|
370
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
371
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: movement.productName ?? `#${movement.id.slice(0, 8)}`, onBack: onClose, parentLabel: t2("inventory.nav.stock") }),
|
|
372
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm p-5 space-y-4", children: [
|
|
373
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
374
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex h-10 w-10 items-center justify-center rounded-xl ${typeConfig?.activeColor ?? "bg-muted"}`, children: /* @__PURE__ */ jsxRuntime.jsx(TypeIcon, { className: "h-5 w-5" }) }),
|
|
375
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
376
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-lg font-bold", children: movement.productName ?? t2("inventory.stock.unknownProduct") }),
|
|
377
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground capitalize", children: [
|
|
378
|
+
movement.movementType,
|
|
379
|
+
" \xB7 ",
|
|
380
|
+
movement.movementDate
|
|
381
|
+
] })
|
|
382
|
+
] })
|
|
383
|
+
] }),
|
|
384
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 sm:grid-cols-3 pt-2 border-t", children: [
|
|
385
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
386
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.quantityLabel") }),
|
|
387
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm font-semibold", children: [
|
|
388
|
+
movement.movementType === "entry" ? "+" : "-",
|
|
389
|
+
movement.quantity
|
|
390
|
+
] })
|
|
391
|
+
] }),
|
|
392
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
393
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.unitCostLabel") }),
|
|
394
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold", children: saas.formatCurrency(movement.unitCost, currency) })
|
|
395
|
+
] }),
|
|
396
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
397
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.totalCost") }),
|
|
398
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold", children: saas.formatCurrency(movement.totalCost, currency) })
|
|
399
|
+
] })
|
|
400
|
+
] }),
|
|
401
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 sm:grid-cols-2 pt-2 border-t", children: [
|
|
402
|
+
movement.stockLocationName && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
403
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.locationLabel") }),
|
|
404
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: movement.stockLocationName })
|
|
405
|
+
] }),
|
|
406
|
+
movement.destinationLocationName && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
407
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.destination") }),
|
|
408
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: movement.destinationLocationName })
|
|
409
|
+
] }),
|
|
410
|
+
movement.supplierName && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
411
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.supplier") }),
|
|
412
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: movement.supplierName })
|
|
413
|
+
] }),
|
|
414
|
+
movement.documentNumber && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
415
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.document") }),
|
|
416
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: movement.documentNumber })
|
|
417
|
+
] }),
|
|
418
|
+
movement.reason && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
419
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.reasonLabel") }),
|
|
420
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: movement.reason })
|
|
421
|
+
] })
|
|
422
|
+
] }),
|
|
423
|
+
movement.notes && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 border-t", children: [
|
|
424
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground uppercase", children: t2("inventory.stock.notes") }),
|
|
425
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm mt-0.5", children: movement.notes })
|
|
426
|
+
] }),
|
|
427
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "pt-2 border-t", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
428
|
+
t2("inventory.stock.recorded"),
|
|
429
|
+
" ",
|
|
430
|
+
new Date(movement.createdAt).toLocaleString()
|
|
431
|
+
] }) })
|
|
432
|
+
] })
|
|
433
|
+
] });
|
|
434
|
+
}
|
|
435
|
+
function StockMovementView({ defaultType, onSaved, viewMovement }) {
|
|
436
|
+
const t2 = core.useTranslation();
|
|
437
|
+
const { currency, modules } = useInventoryConfig();
|
|
438
|
+
const provider = useInventoryProvider();
|
|
439
|
+
const products = useInventoryStore((s) => s.products);
|
|
440
|
+
const locations = useInventoryStore((s) => s.locations);
|
|
441
|
+
const fetchProducts = useInventoryStore((s) => s.fetchProducts);
|
|
442
|
+
const fetchLocations = useInventoryStore((s) => s.fetchLocations);
|
|
443
|
+
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(() => {
|
|
461
|
+
fetchProducts({});
|
|
462
|
+
fetchLocations();
|
|
463
|
+
}, []);
|
|
464
|
+
if (viewMovement) {
|
|
465
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MovementDetail, { movement: viewMovement, currency, onClose: () => onSaved?.() });
|
|
466
|
+
}
|
|
467
|
+
if (savedMovement) {
|
|
468
|
+
return /* @__PURE__ */ jsxRuntime.jsx(MovementDetail, { movement: savedMovement, currency, onClose: () => onSaved?.() });
|
|
469
|
+
}
|
|
470
|
+
const selectedProduct = products.find((p) => p.id === productId);
|
|
471
|
+
const totalCost = quantity * unitCost;
|
|
472
|
+
const typeConfig = MOVEMENT_TYPES.find((t3) => t3.value === movementType);
|
|
473
|
+
const needsReason = movementType === "adjustment" || movementType === "loss";
|
|
474
|
+
const needsDest = movementType === "transfer";
|
|
475
|
+
function searchProducts(query) {
|
|
476
|
+
const q = query.toLowerCase();
|
|
477
|
+
return products.filter((p) => p.name.toLowerCase().includes(q) || p.sku?.toLowerCase().includes(q) || p.barcode?.includes(q)).slice(0, 20).map((p) => ({
|
|
478
|
+
id: p.id,
|
|
479
|
+
label: p.name,
|
|
480
|
+
subtitle: [p.sku, `${t2("inventory.stock.currentStock")} ${p.currentQuantity}`].filter(Boolean).join(" \xB7 "),
|
|
481
|
+
group: p.productType,
|
|
482
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Package, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
483
|
+
data: p
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
function handleProductSelect(id, option) {
|
|
487
|
+
setProductId(id);
|
|
488
|
+
setProductLabel(option?.label ?? "");
|
|
489
|
+
if (option?.data?.costPrice && unitCost === 0) {
|
|
490
|
+
setUnitCost(option.data.costPrice);
|
|
491
|
+
}
|
|
492
|
+
if (id) {
|
|
493
|
+
provider.getPositions(id).then(setPositions).catch(() => setPositions([]));
|
|
494
|
+
} else {
|
|
495
|
+
setPositions([]);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
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
|
+
const canProceedStep1 = !!productId;
|
|
511
|
+
const canProceedStep2 = quantity > 0 && (!needsReason || reason.trim()) && (!needsDest || destLocationId);
|
|
512
|
+
const title = defaultType === "entry" ? t2("inventory.stock.entry") : defaultType === "exit" ? t2("inventory.stock.exit") : t2("inventory.stock.movement");
|
|
513
|
+
async function handleSave() {
|
|
514
|
+
if (!productId || quantity <= 0) return;
|
|
515
|
+
setSaving(true);
|
|
516
|
+
try {
|
|
517
|
+
const movement = await createMovement({
|
|
518
|
+
productId,
|
|
519
|
+
quantity,
|
|
520
|
+
movementType,
|
|
521
|
+
unitCost: unitCost || void 0,
|
|
522
|
+
stockLocationId: locationId || void 0,
|
|
523
|
+
destinationLocationId: destLocationId || void 0,
|
|
524
|
+
reason: reason || void 0,
|
|
525
|
+
documentNumber: documentNumber || void 0,
|
|
526
|
+
notes: notes || void 0,
|
|
527
|
+
batchNumber: batchNumber || void 0,
|
|
528
|
+
expirationDate: expirationDate || void 0
|
|
529
|
+
});
|
|
530
|
+
setSavedMovement(movement);
|
|
531
|
+
} finally {
|
|
532
|
+
setSaving(false);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
536
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
537
|
+
ui.SubpageHeader,
|
|
538
|
+
{
|
|
539
|
+
title,
|
|
540
|
+
subtitle: t2("inventory.stock.stepOf", { step: String(step) }),
|
|
541
|
+
onBack: step > 1 ? () => setStep(step - 1) : onSaved,
|
|
542
|
+
parentLabel: t2("inventory.nav.stock")
|
|
543
|
+
}
|
|
544
|
+
),
|
|
545
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", children: [1, 2, 3].map((s) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: `h-1 flex-1 rounded-full transition-colors ${s <= step ? "bg-primary" : "bg-muted"}` }, s)) }),
|
|
546
|
+
step === 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
547
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
548
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-muted-foreground mb-2", children: t2("inventory.stock.movementType") }),
|
|
549
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-2 sm:grid-cols-5", children: MOVEMENT_TYPES.map((mt) => {
|
|
550
|
+
const Icon = mt.icon;
|
|
551
|
+
const active = movementType === mt.value;
|
|
552
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
553
|
+
"button",
|
|
554
|
+
{
|
|
555
|
+
onClick: () => setMovementType(mt.value),
|
|
556
|
+
className: `rounded-lg border-2 p-3 text-left transition-all ${active ? mt.activeColor : "border-transparent bg-card hover:bg-muted/30"}`,
|
|
557
|
+
children: [
|
|
558
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4 mb-1" }),
|
|
559
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium", children: t2(mt.labelKey) }),
|
|
560
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[9px] text-muted-foreground mt-0.5 hidden sm:block", children: t2(mt.descKey) })
|
|
561
|
+
]
|
|
562
|
+
},
|
|
563
|
+
mt.value
|
|
564
|
+
);
|
|
565
|
+
}) })
|
|
566
|
+
] }),
|
|
567
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
568
|
+
ui.SearchSelect,
|
|
569
|
+
{
|
|
570
|
+
value: productId,
|
|
571
|
+
displayValue: productLabel,
|
|
572
|
+
onChange: handleProductSelect,
|
|
573
|
+
onSearch: searchProducts,
|
|
574
|
+
label: t2("inventory.stock.product"),
|
|
575
|
+
required: true,
|
|
576
|
+
placeholder: t2("inventory.stock.searchProduct"),
|
|
577
|
+
minChars: 1,
|
|
578
|
+
debounce: 150,
|
|
579
|
+
renderOption: (opt) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
580
|
+
opt.icon,
|
|
581
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
582
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm truncate", children: opt.label }),
|
|
583
|
+
opt.subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground", children: opt.subtitle })
|
|
584
|
+
] })
|
|
585
|
+
] })
|
|
586
|
+
}
|
|
587
|
+
),
|
|
588
|
+
selectedProduct && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg bg-muted/30 px-4 py-3 flex items-center justify-between", children: [
|
|
589
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
590
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: selectedProduct.name }),
|
|
591
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground", children: [
|
|
592
|
+
t2("inventory.stock.currentStock"),
|
|
593
|
+
" ",
|
|
594
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: selectedProduct.currentQuantity }),
|
|
595
|
+
selectedProduct.minQuantity > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
596
|
+
" \xB7 Min: ",
|
|
597
|
+
selectedProduct.minQuantity
|
|
598
|
+
] }),
|
|
599
|
+
selectedProduct.costPrice > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
600
|
+
" \xB7 Cost: ",
|
|
601
|
+
saas.formatCurrency(selectedProduct.costPrice, currency)
|
|
602
|
+
] })
|
|
603
|
+
] })
|
|
604
|
+
] }),
|
|
605
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex items-center rounded-full px-2 py-0.5 text-[9px] font-medium bg-muted text-muted-foreground capitalize", children: selectedProduct.productType })
|
|
606
|
+
] }),
|
|
607
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
608
|
+
"button",
|
|
609
|
+
{
|
|
610
|
+
onClick: () => setStep(2),
|
|
611
|
+
disabled: !canProceedStep1,
|
|
612
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors disabled:opacity-50",
|
|
613
|
+
children: t2("inventory.stock.next")
|
|
614
|
+
}
|
|
615
|
+
) })
|
|
616
|
+
] }),
|
|
617
|
+
step === 2 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
618
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm p-5 space-y-4", children: [
|
|
619
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 sm:grid-cols-3", children: [
|
|
620
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
621
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.quantity") }),
|
|
622
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
623
|
+
"input",
|
|
624
|
+
{
|
|
625
|
+
type: "number",
|
|
626
|
+
min: 0.01,
|
|
627
|
+
step: 0.01,
|
|
628
|
+
value: quantity,
|
|
629
|
+
onChange: (e) => setQuantity(Number(e.target.value) || 0),
|
|
630
|
+
autoFocus: true,
|
|
631
|
+
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"
|
|
632
|
+
}
|
|
633
|
+
)
|
|
634
|
+
] }),
|
|
635
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
636
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.unitCost") }),
|
|
637
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus-within:ring-2 focus-within:ring-primary/20", children: [
|
|
638
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "pl-2.5 text-xs text-muted-foreground", children: currency.symbol }),
|
|
639
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "number", min: 0, step: 0.01, value: unitCost, onChange: (e) => setUnitCost(Number(e.target.value) || 0), className: "flex-1 bg-transparent px-2 py-2 text-sm outline-none" })
|
|
640
|
+
] })
|
|
641
|
+
] }),
|
|
642
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
643
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.total") }),
|
|
644
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 px-3 py-2 text-lg font-bold", children: saas.formatCurrency(totalCost, currency) })
|
|
645
|
+
] })
|
|
646
|
+
] }),
|
|
647
|
+
modules.stockLocations && locations.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
648
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
649
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: needsDest ? t2("inventory.stock.fromLocation") : t2("inventory.stock.locationLabel") }),
|
|
650
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 mt-1.5", children: sortedLocations.map((l) => {
|
|
651
|
+
const qty = positionByLocation[l.id] ?? 0;
|
|
652
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
653
|
+
"button",
|
|
654
|
+
{
|
|
655
|
+
type: "button",
|
|
656
|
+
onClick: () => setLocationId(l.id),
|
|
657
|
+
className: ui.cn(
|
|
658
|
+
"rounded-lg border-2 px-3 py-2 text-sm font-medium transition-all flex items-center gap-2",
|
|
659
|
+
locationId === l.id ? "border-primary bg-primary/5 text-primary" : "border-transparent bg-muted/30 text-muted-foreground hover:bg-muted/50"
|
|
660
|
+
),
|
|
661
|
+
children: [
|
|
662
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: l.name }),
|
|
663
|
+
productId && /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn(
|
|
664
|
+
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold tabular-nums",
|
|
665
|
+
qty > 0 ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
|
|
666
|
+
), children: qty })
|
|
667
|
+
]
|
|
668
|
+
},
|
|
669
|
+
l.id
|
|
670
|
+
);
|
|
671
|
+
}) })
|
|
672
|
+
] }),
|
|
673
|
+
needsDest && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
674
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.toLocation") }),
|
|
675
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 mt-1.5", children: sortedLocations.filter((l) => l.id !== locationId).map((l) => {
|
|
676
|
+
const qty = positionByLocation[l.id] ?? 0;
|
|
677
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
678
|
+
"button",
|
|
679
|
+
{
|
|
680
|
+
type: "button",
|
|
681
|
+
onClick: () => setDestLocationId(l.id),
|
|
682
|
+
className: ui.cn(
|
|
683
|
+
"rounded-lg border-2 px-3 py-2 text-sm font-medium transition-all flex items-center gap-2",
|
|
684
|
+
destLocationId === l.id ? "border-primary bg-primary/5 text-primary" : "border-transparent bg-muted/30 text-muted-foreground hover:bg-muted/50"
|
|
685
|
+
),
|
|
686
|
+
children: [
|
|
687
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: l.name }),
|
|
688
|
+
productId && /* @__PURE__ */ jsxRuntime.jsx("span", { className: ui.cn(
|
|
689
|
+
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold tabular-nums",
|
|
690
|
+
qty > 0 ? "bg-success/10 text-success" : "bg-muted text-muted-foreground"
|
|
691
|
+
), children: qty })
|
|
692
|
+
]
|
|
693
|
+
},
|
|
694
|
+
l.id
|
|
695
|
+
);
|
|
696
|
+
}) })
|
|
697
|
+
] })
|
|
698
|
+
] }),
|
|
699
|
+
needsReason && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
700
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.reason") }),
|
|
701
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", value: reason, onChange: (e) => setReason(e.target.value), placeholder: movementType === "loss" ? t2("inventory.stock.reasonLossPlaceholder") : t2("inventory.stock.reasonAdjustPlaceholder"), 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" })
|
|
702
|
+
] })
|
|
703
|
+
] }),
|
|
704
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
|
|
705
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setStep(1), className: "rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors", children: t2("inventory.stock.back") }),
|
|
706
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setStep(3), disabled: !canProceedStep2, className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors disabled:opacity-50", children: t2("inventory.stock.next") })
|
|
707
|
+
] })
|
|
708
|
+
] }),
|
|
709
|
+
step === 3 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
710
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm p-5", children: [
|
|
711
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
|
|
712
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: `flex h-10 w-10 items-center justify-center rounded-xl ${typeConfig.activeColor}`, children: /* @__PURE__ */ jsxRuntime.jsx(typeConfig.icon, { className: "h-5 w-5" }) }),
|
|
713
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
714
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-semibold", children: productLabel }),
|
|
715
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground capitalize", children: [
|
|
716
|
+
movementType,
|
|
717
|
+
" \xB7 ",
|
|
718
|
+
quantity,
|
|
719
|
+
" units \xB7 ",
|
|
720
|
+
saas.formatCurrency(totalCost, currency)
|
|
721
|
+
] })
|
|
722
|
+
] })
|
|
723
|
+
] }),
|
|
724
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-2 sm:grid-cols-3 text-xs border-t pt-3", children: [
|
|
725
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
726
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
727
|
+
t2("inventory.stock.quantityLabel"),
|
|
728
|
+
":"
|
|
729
|
+
] }),
|
|
730
|
+
" ",
|
|
731
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: quantity })
|
|
732
|
+
] }),
|
|
733
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
734
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
735
|
+
t2("inventory.stock.unitCostLabel"),
|
|
736
|
+
":"
|
|
737
|
+
] }),
|
|
738
|
+
" ",
|
|
739
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: saas.formatCurrency(unitCost, currency) })
|
|
740
|
+
] }),
|
|
741
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
742
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
743
|
+
t2("inventory.stock.total"),
|
|
744
|
+
":"
|
|
745
|
+
] }),
|
|
746
|
+
" ",
|
|
747
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-bold", children: saas.formatCurrency(totalCost, currency) })
|
|
748
|
+
] })
|
|
749
|
+
] })
|
|
750
|
+
] }),
|
|
751
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm p-5 space-y-4", children: [
|
|
752
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.additionalDetails") }),
|
|
753
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
|
|
754
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
755
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.documentNumber") }),
|
|
756
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", value: documentNumber, onChange: (e) => setDocumentNumber(e.target.value), placeholder: t2("inventory.stock.documentPlaceholder"), 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" })
|
|
757
|
+
] }),
|
|
758
|
+
modules.batchTracking && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
759
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
760
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.batchNumber") }),
|
|
761
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", value: batchNumber, onChange: (e) => setBatchNumber(e.target.value), placeholder: t2("inventory.stock.batchPlaceholder"), 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" })
|
|
762
|
+
] }),
|
|
763
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
764
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.expirationDate") }),
|
|
765
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DatePicker, { value: expirationDate, onChange: setExpirationDate, className: "mt-1" })
|
|
766
|
+
] })
|
|
767
|
+
] })
|
|
768
|
+
] }),
|
|
769
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
770
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.stock.notes") }),
|
|
771
|
+
/* @__PURE__ */ jsxRuntime.jsx("textarea", { value: notes, onChange: (e) => setNotes(e.target.value), rows: 2, placeholder: t2("inventory.stock.notesPlaceholder"), 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" })
|
|
772
|
+
] })
|
|
773
|
+
] }),
|
|
774
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between", children: [
|
|
775
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => setStep(2), className: "rounded-lg border px-4 py-2 text-sm font-medium hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors", children: t2("inventory.stock.back") }),
|
|
776
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
777
|
+
"button",
|
|
778
|
+
{
|
|
779
|
+
onClick: handleSave,
|
|
780
|
+
disabled: saving,
|
|
781
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-5 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors disabled:opacity-50",
|
|
782
|
+
children: [
|
|
783
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5" }),
|
|
784
|
+
" ",
|
|
785
|
+
saving ? t2("inventory.stock.recording") : t2("inventory.stock.recordMovement")
|
|
786
|
+
]
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
] })
|
|
790
|
+
] })
|
|
791
|
+
] });
|
|
792
|
+
}
|
|
793
|
+
var TYPE_ICONS = {
|
|
794
|
+
entry: lucideReact.ArrowDownRight,
|
|
795
|
+
exit: lucideReact.ArrowUpRight,
|
|
796
|
+
adjustment: lucideReact.RefreshCw,
|
|
797
|
+
transfer: lucideReact.ArrowRightLeft,
|
|
798
|
+
loss: lucideReact.Trash2
|
|
799
|
+
};
|
|
800
|
+
var TYPE_COLORS = {
|
|
801
|
+
entry: "text-success",
|
|
802
|
+
exit: "text-destructive",
|
|
803
|
+
adjustment: "text-info",
|
|
804
|
+
transfer: "text-magic",
|
|
805
|
+
loss: "text-warning"
|
|
806
|
+
};
|
|
807
|
+
function MovementHistoryView({ onViewDetail } = {}) {
|
|
808
|
+
const t2 = core.useTranslation();
|
|
809
|
+
const { currency } = useInventoryConfig();
|
|
810
|
+
const movements = useInventoryStore((s) => s.movements);
|
|
811
|
+
const movementsLoading = useInventoryStore((s) => s.movementsLoading);
|
|
812
|
+
const fetchMovements = useInventoryStore((s) => s.fetchMovements);
|
|
813
|
+
const [search, setSearch] = React11.useState("");
|
|
814
|
+
React11.useEffect(() => {
|
|
815
|
+
fetchMovements({ search: search || void 0 });
|
|
816
|
+
}, [search]);
|
|
817
|
+
const columns = React11.useMemo(() => [
|
|
818
|
+
{
|
|
819
|
+
accessorKey: "movementDate",
|
|
820
|
+
header: t2("inventory.history.columnDate"),
|
|
821
|
+
cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: getValue() })
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
accessorKey: "productName",
|
|
825
|
+
header: t2("inventory.history.columnProduct"),
|
|
826
|
+
cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: getValue() ?? "\u2014" })
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
accessorKey: "movementType",
|
|
830
|
+
header: t2("inventory.history.columnType"),
|
|
831
|
+
cell: ({ getValue }) => {
|
|
832
|
+
const type = getValue();
|
|
833
|
+
const Icon = TYPE_ICONS[type] ?? lucideReact.RefreshCw;
|
|
834
|
+
const color = TYPE_COLORS[type] ?? "text-muted-foreground";
|
|
835
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `inline-flex items-center gap-1 ${color}`, children: [
|
|
836
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-3 w-3" }),
|
|
837
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs capitalize", children: type })
|
|
838
|
+
] });
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
id: "location",
|
|
843
|
+
header: t2("inventory.history.columnLocation"),
|
|
844
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
845
|
+
row.original.stockLocationName ?? "\u2014",
|
|
846
|
+
row.original.movementType === "transfer" && row.original.destinationLocationName && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
847
|
+
" \u2192 ",
|
|
848
|
+
row.original.destinationLocationName
|
|
849
|
+
] })
|
|
850
|
+
] })
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
accessorKey: "quantity",
|
|
854
|
+
header: t2("inventory.history.columnQty"),
|
|
855
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-right block", children: [
|
|
856
|
+
row.original.movementType === "entry" ? "+" : "-",
|
|
857
|
+
row.original.quantity
|
|
858
|
+
] })
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
accessorKey: "totalCost",
|
|
862
|
+
header: t2("inventory.history.columnTotal"),
|
|
863
|
+
cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-muted-foreground", children: saas.formatCurrency(getValue(), currency) })
|
|
864
|
+
}
|
|
865
|
+
], [currency, t2]);
|
|
866
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
867
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: t2("inventory.history.title"), subtitle: t2("inventory.history.subtitle") }),
|
|
868
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
869
|
+
ui.ListView,
|
|
870
|
+
{
|
|
871
|
+
columns,
|
|
872
|
+
data: movements,
|
|
873
|
+
loading: movementsLoading,
|
|
874
|
+
searchPlaceholder: t2("inventory.history.searchPlaceholder"),
|
|
875
|
+
search,
|
|
876
|
+
onSearchChange: setSearch,
|
|
877
|
+
searchDebounce: 0,
|
|
878
|
+
onRowClick: onViewDetail ? (row) => onViewDetail(row.id) : void 0,
|
|
879
|
+
emptyMessage: t2("inventory.history.noMovements")
|
|
880
|
+
}
|
|
881
|
+
)
|
|
882
|
+
] });
|
|
883
|
+
}
|
|
884
|
+
function RecipeSkeleton() {
|
|
885
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: [1, 2, 3, 4, 5, 6].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm p-4 space-y-3", children: [
|
|
886
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
887
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-9 w-9 rounded-lg bg-muted/40 animate-pulse" }),
|
|
888
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-1.5", children: [
|
|
889
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-2/3 rounded bg-muted/40 animate-pulse" }),
|
|
890
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-1/2 rounded bg-muted/30 animate-pulse" })
|
|
891
|
+
] })
|
|
892
|
+
] }),
|
|
893
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-3", children: [
|
|
894
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-20 rounded bg-muted/30 animate-pulse" }),
|
|
895
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-16 rounded bg-muted/30 animate-pulse" })
|
|
896
|
+
] })
|
|
897
|
+
] }, i)) });
|
|
898
|
+
}
|
|
899
|
+
function RecipesView({ onNew, onView }) {
|
|
900
|
+
const t2 = core.useTranslation();
|
|
901
|
+
const recipes = useInventoryStore((s) => s.recipes);
|
|
902
|
+
const recipesLoading = useInventoryStore((s) => s.recipesLoading);
|
|
903
|
+
const fetchRecipes = useInventoryStore((s) => s.fetchRecipes);
|
|
904
|
+
React11.useEffect(() => {
|
|
905
|
+
fetchRecipes();
|
|
906
|
+
}, []);
|
|
907
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
908
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
909
|
+
ui.SubpageHeader,
|
|
910
|
+
{
|
|
911
|
+
title: t2("inventory.recipes.title"),
|
|
912
|
+
subtitle: t2("inventory.recipes.productionFormulas", { count: String(recipes.length) }),
|
|
913
|
+
actions: onNew && /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: onNew, className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors", children: [
|
|
914
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
|
|
915
|
+
" ",
|
|
916
|
+
t2("inventory.recipes.newRecipe")
|
|
917
|
+
] })
|
|
918
|
+
}
|
|
919
|
+
),
|
|
920
|
+
recipesLoading ? /* @__PURE__ */ jsxRuntime.jsx(RecipeSkeleton, {}) : recipes.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
921
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-2xl bg-muted/30 mb-3", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.BookOpen, { className: "h-5 w-5 text-muted-foreground/40" }) }),
|
|
922
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("inventory.recipes.noRecipes") }),
|
|
923
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground/60 mt-0.5", children: t2("inventory.recipes.recipesDesc") }),
|
|
924
|
+
onNew && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onNew, className: "text-xs text-primary hover:underline mt-2", children: t2("inventory.recipes.createFirst") })
|
|
925
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: recipes.map((r) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
926
|
+
"div",
|
|
927
|
+
{
|
|
928
|
+
onClick: () => onView?.(r.id),
|
|
929
|
+
className: "rounded-lg border bg-card p-4 hover:shadow-sm hover:border-primary/20 transition-all cursor-pointer group",
|
|
930
|
+
children: [
|
|
931
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
932
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary shrink-0 group-hover:bg-primary/15 transition-colors", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.BookOpen, { className: "h-4 w-4" }) }),
|
|
933
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
|
|
934
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold truncate", children: r.name }),
|
|
935
|
+
r.productName && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[10px] text-muted-foreground truncate", children: [
|
|
936
|
+
t2("inventory.recipes.produces"),
|
|
937
|
+
" ",
|
|
938
|
+
r.productName
|
|
939
|
+
] })
|
|
940
|
+
] })
|
|
941
|
+
] }),
|
|
942
|
+
r.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-2 line-clamp-2", children: r.description }),
|
|
943
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 mt-3 pt-2.5 border-t", children: [
|
|
944
|
+
r.ingredientCount != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-muted-foreground", children: [
|
|
945
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Layers, { className: "h-3 w-3" }),
|
|
946
|
+
" ",
|
|
947
|
+
t2("inventory.recipes.ingredients", { count: String(r.ingredientCount) })
|
|
948
|
+
] }),
|
|
949
|
+
r.preparationTimeMinutes != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-muted-foreground", children: [
|
|
950
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-3 w-3" }),
|
|
951
|
+
" ",
|
|
952
|
+
r.preparationTimeMinutes,
|
|
953
|
+
" min"
|
|
954
|
+
] }),
|
|
955
|
+
r.yieldQuantity > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[10px] text-muted-foreground ml-auto", children: [
|
|
956
|
+
t2("inventory.recipes.yield"),
|
|
957
|
+
" ",
|
|
958
|
+
r.yieldQuantity,
|
|
959
|
+
r.yieldUnitName ? ` ${r.yieldUnitName}` : ""
|
|
960
|
+
] })
|
|
961
|
+
] })
|
|
962
|
+
]
|
|
963
|
+
},
|
|
964
|
+
r.id
|
|
965
|
+
)) })
|
|
966
|
+
] });
|
|
967
|
+
}
|
|
968
|
+
var fid = 1;
|
|
969
|
+
function nextId() {
|
|
970
|
+
return `ri${fid++}`;
|
|
971
|
+
}
|
|
972
|
+
function RecipeFormView({ onSaved }) {
|
|
973
|
+
const t2 = core.useTranslation();
|
|
974
|
+
const provider = useInventoryProvider();
|
|
975
|
+
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);
|
|
985
|
+
function addIngredient() {
|
|
986
|
+
setIngredients([...ingredients, { _id: nextId(), productId: "", productName: "", quantity: 1, unitId: "", unitName: "", notes: "" }]);
|
|
987
|
+
}
|
|
988
|
+
function updateIngredient(id, data) {
|
|
989
|
+
setIngredients(ingredients.map((ing) => ing._id === id ? { ...ing, ...data } : ing));
|
|
990
|
+
}
|
|
991
|
+
function removeIngredient(id) {
|
|
992
|
+
setIngredients(ingredients.filter((ing) => ing._id !== id));
|
|
993
|
+
}
|
|
994
|
+
async function handleSave() {
|
|
995
|
+
if (!name.trim() || !productId || ingredients.length === 0) {
|
|
996
|
+
ui.toast.error(t2("common.formIncomplete"));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
setSaving(true);
|
|
1000
|
+
try {
|
|
1001
|
+
const recipe = await createRecipe({
|
|
1002
|
+
name,
|
|
1003
|
+
description: description || void 0,
|
|
1004
|
+
productId,
|
|
1005
|
+
yieldQuantity,
|
|
1006
|
+
preparationTimeMinutes: prepTime ? parseInt(prepTime) : void 0,
|
|
1007
|
+
instructions: instructions || void 0,
|
|
1008
|
+
ingredients: ingredients.filter((i) => i.productId).map((i, idx) => ({
|
|
1009
|
+
productId: i.productId,
|
|
1010
|
+
quantity: i.quantity,
|
|
1011
|
+
unitId: i.unitId || void 0,
|
|
1012
|
+
displayOrder: idx,
|
|
1013
|
+
notes: i.notes || void 0
|
|
1014
|
+
}))
|
|
1015
|
+
});
|
|
1016
|
+
onSaved?.(recipe.id);
|
|
1017
|
+
} finally {
|
|
1018
|
+
setSaving(false);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const dirty = !!(name || description || productId || prepTime || instructions || ingredients.length > 0 || yieldQuantity !== 1);
|
|
1022
|
+
ui.useSaveBar({
|
|
1023
|
+
dirty,
|
|
1024
|
+
saving,
|
|
1025
|
+
onSave: () => {
|
|
1026
|
+
void handleSave();
|
|
1027
|
+
},
|
|
1028
|
+
onDiscard: () => onSaved?.(),
|
|
1029
|
+
saveLabel: t2("inventory.recipeForm.saveRecipe")
|
|
1030
|
+
});
|
|
1031
|
+
async function searchProducts(query) {
|
|
1032
|
+
const result = await provider.getProducts({ search: query, pageSize: 10 });
|
|
1033
|
+
return result.data.map((p) => ({ id: p.id, label: p.name, subtitle: p.sku ?? p.productType, data: p }));
|
|
1034
|
+
}
|
|
1035
|
+
async function quickCreateProduct(name2, type = "ingredient") {
|
|
1036
|
+
try {
|
|
1037
|
+
const product = await provider.createProduct({ name: name2, productType: type });
|
|
1038
|
+
ui.toast.success(`Product "${name2}" created`);
|
|
1039
|
+
return product;
|
|
1040
|
+
} catch {
|
|
1041
|
+
ui.toast.error("Failed to create product");
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
1046
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1047
|
+
ui.SubpageHeader,
|
|
1048
|
+
{
|
|
1049
|
+
title: t2("inventory.recipeForm.newRecipe"),
|
|
1050
|
+
subtitle: t2("inventory.recipeForm.subtitle"),
|
|
1051
|
+
onBack: () => onSaved?.(),
|
|
1052
|
+
parentLabel: t2("inventory.nav.recipes")
|
|
1053
|
+
}
|
|
1054
|
+
),
|
|
1055
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [
|
|
1056
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm p-5 space-y-4", children: [
|
|
1057
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold", children: t2("inventory.recipeForm.recipeDetails") }),
|
|
1058
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1059
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "text-xs font-medium text-muted-foreground", children: [
|
|
1060
|
+
t2("inventory.recipeForm.recipeName"),
|
|
1061
|
+
" *"
|
|
1062
|
+
] }),
|
|
1063
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: t2("inventory.recipeForm.recipeNamePlaceholder"), autoFocus: true, 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" })
|
|
1064
|
+
] }),
|
|
1065
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1066
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.recipeForm.description") }),
|
|
1067
|
+
/* @__PURE__ */ jsxRuntime.jsx("textarea", { value: description, onChange: (e) => setDescription(e.target.value), rows: 2, placeholder: t2("inventory.recipeForm.descriptionPlaceholder"), 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" })
|
|
1068
|
+
] }),
|
|
1069
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1070
|
+
ui.SearchSelect,
|
|
1071
|
+
{
|
|
1072
|
+
label: `${t2("inventory.recipeForm.produces")} *`,
|
|
1073
|
+
value: productId,
|
|
1074
|
+
displayValue: productName,
|
|
1075
|
+
onChange: (id, opt) => {
|
|
1076
|
+
setProductId(id);
|
|
1077
|
+
setProductName(opt?.label ?? "");
|
|
1078
|
+
},
|
|
1079
|
+
onSearch: searchProducts,
|
|
1080
|
+
placeholder: t2("inventory.recipeForm.searchProduct"),
|
|
1081
|
+
allowCreate: true,
|
|
1082
|
+
createLabel: t2("inventory.recipeForm.createProduct"),
|
|
1083
|
+
onCreate: async (q) => {
|
|
1084
|
+
const p = await quickCreateProduct(q, "sale");
|
|
1085
|
+
if (p) {
|
|
1086
|
+
setProductId(p.id);
|
|
1087
|
+
setProductName(p.name);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
),
|
|
1092
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
1093
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1094
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.recipeForm.yieldQuantity") }),
|
|
1095
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "number", min: 0.01, step: 0.01, value: yieldQuantity, onChange: (e) => setYieldQuantity(Number(e.target.value) || 1), 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" })
|
|
1096
|
+
] }),
|
|
1097
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1098
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.recipeForm.prepTime") }),
|
|
1099
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "number", min: 0, value: prepTime, onChange: (e) => setPrepTime(e.target.value), placeholder: "\u2014", 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" })
|
|
1100
|
+
] })
|
|
1101
|
+
] }),
|
|
1102
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1103
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("inventory.recipeForm.instructions") }),
|
|
1104
|
+
/* @__PURE__ */ jsxRuntime.jsx("textarea", { value: instructions, onChange: (e) => setInstructions(e.target.value), rows: 4, placeholder: t2("inventory.recipeForm.instructionsPlaceholder"), 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" })
|
|
1105
|
+
] })
|
|
1106
|
+
] }),
|
|
1107
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lg:col-span-2 rounded-lg border bg-card shadow-sm overflow-hidden flex flex-col", children: [
|
|
1108
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b", children: [
|
|
1109
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold", children: t2("inventory.recipeForm.ingredients") }),
|
|
1110
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: addIngredient, className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors", children: [
|
|
1111
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3 w-3" }),
|
|
1112
|
+
" ",
|
|
1113
|
+
t2("inventory.recipeForm.addIngredient")
|
|
1114
|
+
] })
|
|
1115
|
+
] }),
|
|
1116
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: ingredients.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-12 text-center", children: [
|
|
1117
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("inventory.recipeForm.noIngredients") }),
|
|
1118
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: addIngredient, className: "text-xs text-primary hover:underline mt-1", children: t2("inventory.recipeForm.addFirstIngredient") })
|
|
1119
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "divide-y", children: [
|
|
1120
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-12 gap-2 px-4 py-2 text-[10px] font-medium text-muted-foreground uppercase tracking-wider bg-muted/20", children: [
|
|
1121
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-5", children: t2("inventory.recipeForm.ingredient") }),
|
|
1122
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: t2("inventory.recipeForm.quantity") }),
|
|
1123
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-4", children: t2("inventory.recipeForm.notes") }),
|
|
1124
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-1" })
|
|
1125
|
+
] }),
|
|
1126
|
+
ingredients.map((ing, idx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-12 gap-2 px-4 py-2.5 items-center group", children: [
|
|
1127
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-5", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1128
|
+
ui.SearchSelect,
|
|
1129
|
+
{
|
|
1130
|
+
value: ing.productId,
|
|
1131
|
+
displayValue: ing.productName,
|
|
1132
|
+
onChange: (id, opt) => updateIngredient(ing._id, { productId: id, productName: opt?.label ?? "" }),
|
|
1133
|
+
onSearch: searchProducts,
|
|
1134
|
+
placeholder: t2("inventory.recipeForm.searchIngredient"),
|
|
1135
|
+
allowCreate: true,
|
|
1136
|
+
createLabel: t2("inventory.recipeForm.createIngredient"),
|
|
1137
|
+
onCreate: async (q) => {
|
|
1138
|
+
const p = await quickCreateProduct(q, "ingredient");
|
|
1139
|
+
if (p) updateIngredient(ing._id, { productId: p.id, productName: p.name });
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
) }),
|
|
1143
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-2", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1144
|
+
"input",
|
|
1145
|
+
{
|
|
1146
|
+
type: "number",
|
|
1147
|
+
min: 0.01,
|
|
1148
|
+
step: 0.01,
|
|
1149
|
+
value: ing.quantity,
|
|
1150
|
+
onChange: (e) => updateIngredient(ing._id, { quantity: Number(e.target.value) || 0 }),
|
|
1151
|
+
className: "w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1152
|
+
}
|
|
1153
|
+
) }),
|
|
1154
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-4", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1155
|
+
"input",
|
|
1156
|
+
{
|
|
1157
|
+
type: "text",
|
|
1158
|
+
value: ing.notes,
|
|
1159
|
+
onChange: (e) => updateIngredient(ing._id, { notes: e.target.value }),
|
|
1160
|
+
placeholder: t2("inventory.recipeForm.notesPlaceholder"),
|
|
1161
|
+
className: "w-full rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
1162
|
+
}
|
|
1163
|
+
) }),
|
|
1164
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-1 flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => removeIngredient(ing._id), className: "p-1 text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-destructive transition-all", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" }) }) })
|
|
1165
|
+
] }, ing._id))
|
|
1166
|
+
] }) }),
|
|
1167
|
+
ingredients.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 border-t bg-muted/20 text-xs text-muted-foreground", children: t2("inventory.recipeForm.ingredientsConfigured", { configured: String(ingredients.filter((i) => i.productId).length), total: String(ingredients.length) }) })
|
|
1168
|
+
] })
|
|
1169
|
+
] })
|
|
1170
|
+
] });
|
|
1171
|
+
}
|
|
1172
|
+
function DetailSkeleton() {
|
|
1173
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
1174
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1175
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-12 w-12 rounded-xl bg-muted/40 animate-pulse" }),
|
|
1176
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 flex-1", children: [
|
|
1177
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-40 rounded bg-muted/40 animate-pulse" }),
|
|
1178
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-24 rounded bg-muted/30 animate-pulse" })
|
|
1179
|
+
] })
|
|
1180
|
+
] }),
|
|
1181
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border divide-y", children: [1, 2, 3].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1182
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-20 rounded bg-muted/30 animate-pulse" }),
|
|
1183
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
|
|
1184
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-16 rounded bg-muted/40 animate-pulse" })
|
|
1185
|
+
] }, i)) }),
|
|
1186
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border p-4 space-y-2", children: [1, 2, 3, 4].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1187
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3.5 w-6 rounded bg-muted/30 animate-pulse" }),
|
|
1188
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3.5 flex-1 rounded bg-muted/30 animate-pulse" }),
|
|
1189
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3.5 w-12 rounded bg-muted/40 animate-pulse" })
|
|
1190
|
+
] }, i)) })
|
|
1191
|
+
] });
|
|
1192
|
+
}
|
|
1193
|
+
function RecipeDetailView({ recipeId, onBack }) {
|
|
1194
|
+
const t2 = core.useTranslation();
|
|
1195
|
+
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(() => {
|
|
1200
|
+
setLoading(true);
|
|
1201
|
+
Promise.all([
|
|
1202
|
+
provider.getRecipeById(recipeId),
|
|
1203
|
+
provider.getRecipeIngredients(recipeId)
|
|
1204
|
+
]).then(([r, ings]) => {
|
|
1205
|
+
setRecipe(r);
|
|
1206
|
+
setIngredients(ings.sort((a, b) => a.displayOrder - b.displayOrder));
|
|
1207
|
+
setLoading(false);
|
|
1208
|
+
});
|
|
1209
|
+
}, [recipeId]);
|
|
1210
|
+
if (loading) {
|
|
1211
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
1212
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Breadcrumb, { parent: t2("inventory.nav.recipes"), current: t2("inventory.recipeDetail.loading"), onBack }),
|
|
1213
|
+
/* @__PURE__ */ jsxRuntime.jsx(DetailSkeleton, {})
|
|
1214
|
+
] });
|
|
1215
|
+
}
|
|
1216
|
+
if (!recipe) {
|
|
1217
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
1218
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Breadcrumb, { parent: t2("inventory.nav.recipes"), onBack }),
|
|
1219
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
1220
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.BookOpen, { className: "h-8 w-8 text-muted-foreground/30 mb-2" }),
|
|
1221
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("inventory.recipeDetail.notFound") }),
|
|
1222
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onBack, className: "text-xs text-primary hover:underline mt-1", children: t2("inventory.recipeDetail.backToList") })
|
|
1223
|
+
] })
|
|
1224
|
+
] });
|
|
1225
|
+
}
|
|
1226
|
+
const ingredientColumns = [
|
|
1227
|
+
{
|
|
1228
|
+
id: "index",
|
|
1229
|
+
header: "#",
|
|
1230
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: row.index + 1 })
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
accessorKey: "productName",
|
|
1234
|
+
header: t2("inventory.recipeDetail.ingredient"),
|
|
1235
|
+
cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: getValue() || "\u2014" })
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
accessorKey: "quantity",
|
|
1239
|
+
header: () => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-right", children: t2("inventory.recipeDetail.quantity") }),
|
|
1240
|
+
cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-right tabular-nums", children: getValue() })
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
accessorKey: "unitName",
|
|
1244
|
+
header: t2("inventory.recipeDetail.unit"),
|
|
1245
|
+
cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: getValue() || "\u2014" })
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
accessorKey: "notes",
|
|
1249
|
+
header: t2("inventory.recipeDetail.notes"),
|
|
1250
|
+
cell: ({ getValue }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: getValue() || "\u2014" })
|
|
1251
|
+
}
|
|
1252
|
+
];
|
|
1253
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
1254
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Breadcrumb, { parent: t2("inventory.nav.recipes"), current: recipe.name, onBack }),
|
|
1255
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-4", children: [
|
|
1256
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-primary", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.BookOpen, { className: "h-6 w-6" }) }),
|
|
1257
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
1258
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-foreground", children: recipe.name }),
|
|
1259
|
+
recipe.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground mt-0.5 text-sm", children: recipe.description }),
|
|
1260
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-3 mt-2", children: recipe.isActive ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-success", children: [
|
|
1261
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-success" }),
|
|
1262
|
+
" ",
|
|
1263
|
+
t2("inventory.recipeDetail.active")
|
|
1264
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 text-[10px] text-muted-foreground/50", children: [
|
|
1265
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground/30" }),
|
|
1266
|
+
" ",
|
|
1267
|
+
t2("inventory.recipeDetail.inactive")
|
|
1268
|
+
] }) })
|
|
1269
|
+
] })
|
|
1270
|
+
] }),
|
|
1271
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t" }),
|
|
1272
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [
|
|
1273
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
1274
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border divide-y", children: [
|
|
1275
|
+
recipe.productName && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1276
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Package, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1277
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground flex-1", children: t2("inventory.recipeDetail.produces") }),
|
|
1278
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: recipe.productName })
|
|
1279
|
+
] }),
|
|
1280
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1281
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Layers, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1282
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground flex-1", children: t2("inventory.recipeDetail.yield") }),
|
|
1283
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium", children: [
|
|
1284
|
+
recipe.yieldQuantity,
|
|
1285
|
+
recipe.yieldUnitName ? ` ${recipe.yieldUnitName}` : ""
|
|
1286
|
+
] })
|
|
1287
|
+
] }),
|
|
1288
|
+
recipe.preparationTimeMinutes != null && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1289
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Clock, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1290
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground flex-1", children: t2("inventory.recipeDetail.prepTime") }),
|
|
1291
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm font-medium", children: [
|
|
1292
|
+
recipe.preparationTimeMinutes,
|
|
1293
|
+
" min"
|
|
1294
|
+
] })
|
|
1295
|
+
] }),
|
|
1296
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [
|
|
1297
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Layers, { className: "h-4 w-4 text-muted-foreground shrink-0" }),
|
|
1298
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground flex-1", children: t2("inventory.recipeDetail.ingredientCount") }),
|
|
1299
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: ingredients.length })
|
|
1300
|
+
] })
|
|
1301
|
+
] }),
|
|
1302
|
+
recipe.instructions && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1303
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-foreground mb-1", children: t2("inventory.recipeDetail.instructions") }),
|
|
1304
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border bg-card shadow-sm px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground whitespace-pre-wrap leading-relaxed", children: recipe.instructions }) })
|
|
1305
|
+
] }),
|
|
1306
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 text-[10px] text-muted-foreground/50", children: [
|
|
1307
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
1308
|
+
t2("inventory.recipeDetail.created"),
|
|
1309
|
+
" ",
|
|
1310
|
+
recipe.createdAt?.slice(0, 10)
|
|
1311
|
+
] }),
|
|
1312
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
1313
|
+
t2("inventory.recipeDetail.updated"),
|
|
1314
|
+
" ",
|
|
1315
|
+
recipe.updatedAt?.slice(0, 10)
|
|
1316
|
+
] })
|
|
1317
|
+
] })
|
|
1318
|
+
] }),
|
|
1319
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lg:col-span-2", children: [
|
|
1320
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-foreground mb-2", children: t2("inventory.recipeDetail.ingredientsTitle") }),
|
|
1321
|
+
ingredients.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border p-8 text-center", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t2("inventory.recipeDetail.noIngredients") }) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns: ingredientColumns, data: ingredients, variant: "card" })
|
|
1322
|
+
] })
|
|
1323
|
+
] })
|
|
1324
|
+
] });
|
|
1325
|
+
}
|
|
1326
|
+
function InventoryGeneralSettings() {
|
|
1327
|
+
const t2 = core.useTranslation();
|
|
1328
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
1329
|
+
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
|
|
1330
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: true, onChange: () => {
|
|
1331
|
+
} }),
|
|
1332
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: true, onChange: () => {
|
|
1333
|
+
} }),
|
|
1334
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: false, onChange: () => {
|
|
1335
|
+
} })
|
|
1336
|
+
] }),
|
|
1337
|
+
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.products"), description: t2("inventory.settings.productsDesc"), children: [
|
|
1338
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: false, onChange: () => {
|
|
1339
|
+
} }),
|
|
1340
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: false, onChange: () => {
|
|
1341
|
+
} })
|
|
1342
|
+
] }),
|
|
1343
|
+
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.notifications"), description: t2("inventory.settings.notificationsDesc"), children: [
|
|
1344
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: false, onChange: () => {
|
|
1345
|
+
} }),
|
|
1346
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: true, onChange: () => {
|
|
1347
|
+
} })
|
|
1348
|
+
] })
|
|
1349
|
+
] });
|
|
1350
|
+
}
|
|
1351
|
+
var STEPS = [
|
|
1352
|
+
{ id: "welcome", icon: lucideReact.Package, titleKey: "inventory.onboarding.welcome", descKey: "inventory.onboarding.description" },
|
|
1353
|
+
{ id: "units", icon: lucideReact.Ruler, titleKey: "inventory.onboarding.units.title", descKey: "inventory.onboarding.units.description" },
|
|
1354
|
+
{ id: "locations", icon: lucideReact.MapPin, titleKey: "inventory.onboarding.locations.title", descKey: "inventory.onboarding.locations.description" }
|
|
1355
|
+
];
|
|
1356
|
+
function InventoryOnboarding({ onComplete }) {
|
|
1357
|
+
const t2 = core.useTranslation();
|
|
1358
|
+
const [step, setStep] = React11.useState(0);
|
|
1359
|
+
const current = STEPS[step];
|
|
1360
|
+
const isLast = step === STEPS.length - 1;
|
|
1361
|
+
const Icon = current.icon;
|
|
1362
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center py-12", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-lg", children: [
|
|
1363
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1 mb-8", children: STEPS.map((_, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: `h-1 flex-1 rounded-full transition-colors ${i <= step ? "bg-primary" : "bg-muted"}` }, i)) }),
|
|
1364
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-card border bg-card p-8 text-center space-y-6 shadow-sm", children: [
|
|
1365
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-8 w-8 text-primary" }) }) }),
|
|
1366
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1367
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-xl font-bold", children: t2(current.titleKey) }),
|
|
1368
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mt-2 max-w-sm mx-auto", children: t2(current.descKey) })
|
|
1369
|
+
] }),
|
|
1370
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-3 pt-2", children: [
|
|
1371
|
+
step > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "lg", onClick: () => setStep(step - 1), children: t2("common.back") }),
|
|
1372
|
+
isLast ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "default", size: "lg", onClick: onComplete, children: [
|
|
1373
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4" }),
|
|
1374
|
+
" ",
|
|
1375
|
+
t2("inventory.onboarding.start")
|
|
1376
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "default", size: "lg", onClick: () => setStep(step + 1), children: [
|
|
1377
|
+
step === 0 ? t2("inventory.onboarding.getStarted") : t2("common.continue"),
|
|
1378
|
+
" ",
|
|
1379
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" })
|
|
1380
|
+
] })
|
|
1381
|
+
] }),
|
|
1382
|
+
step === 0 && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onComplete, className: "text-xs text-muted-foreground hover:text-foreground transition-colors", children: t2("inventory.onboarding.skip") })
|
|
1383
|
+
] })
|
|
1384
|
+
] }) });
|
|
1385
|
+
}
|
|
1386
|
+
function buildNav(config, view, navigate, t2) {
|
|
1387
|
+
const items = [
|
|
1388
|
+
{ id: "dashboard", label: t2("inventory.nav.dashboard"), icon: "BarChart3", active: view === "dashboard", onClick: () => navigate("dashboard") },
|
|
1389
|
+
{
|
|
1390
|
+
id: "products",
|
|
1391
|
+
label: t2("inventory.nav.products"),
|
|
1392
|
+
icon: "Package",
|
|
1393
|
+
active: view.startsWith("products"),
|
|
1394
|
+
children: [
|
|
1395
|
+
{ id: "products-new", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
|
|
1396
|
+
{ id: "products-list", label: t2("inventory.nav.list"), active: view === "products-list", onClick: () => navigate("products-list") }
|
|
1397
|
+
]
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
id: "stock",
|
|
1401
|
+
label: t2("inventory.nav.stock"),
|
|
1402
|
+
icon: "ArrowUpCircle",
|
|
1403
|
+
children: [
|
|
1404
|
+
{ id: "stock-entry", label: t2("inventory.nav.entry"), active: view === "stock-entry", onClick: () => navigate("stock-entry") },
|
|
1405
|
+
{ id: "stock-exit", label: t2("inventory.nav.exit"), active: view === "stock-exit", onClick: () => navigate("stock-exit") },
|
|
1406
|
+
{ id: "stock-history", label: t2("inventory.nav.history"), active: view === "stock-history", onClick: () => navigate("stock-history") }
|
|
1407
|
+
]
|
|
1408
|
+
}
|
|
1409
|
+
];
|
|
1410
|
+
if (config.modules.recipes) {
|
|
1411
|
+
items.push({
|
|
1412
|
+
id: "recipes",
|
|
1413
|
+
label: config.labels.recipes,
|
|
1414
|
+
icon: "BookOpen",
|
|
1415
|
+
children: [
|
|
1416
|
+
{ id: "recipes-list", label: config.labels.recipesList, active: view === "recipes-list" || view.startsWith("recipes-detail:"), onClick: () => navigate("recipes-list") },
|
|
1417
|
+
{ id: "recipes-new", label: config.labels.recipesNew, active: view === "recipes-new", onClick: () => navigate("recipes-new") }
|
|
1418
|
+
]
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
return items;
|
|
1422
|
+
}
|
|
1423
|
+
function InventoryPage({ config, provider, store, registries }) {
|
|
1424
|
+
const t2 = core.useTranslation();
|
|
1425
|
+
const { view, direction, navigate } = saas.useModuleNavigation("/inventory", {
|
|
1426
|
+
dashboard: 0,
|
|
1427
|
+
"products-list": 0,
|
|
1428
|
+
"products-new": 1,
|
|
1429
|
+
"stock-entry": 1,
|
|
1430
|
+
"stock-exit": 1,
|
|
1431
|
+
"stock-history": 0,
|
|
1432
|
+
"recipes-list": 0,
|
|
1433
|
+
"recipes-new": 1,
|
|
1434
|
+
"recipes-detail": 1,
|
|
1435
|
+
settings: 1
|
|
1436
|
+
}, "dashboard");
|
|
1437
|
+
const [onboardingComplete, setOnboardingComplete] = React11.useState(() => {
|
|
1438
|
+
try {
|
|
1439
|
+
return localStorage.getItem("saas-core:inventory-onboarded") === "true";
|
|
1440
|
+
} catch {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
const isSettings = view === "settings";
|
|
1445
|
+
const isSummary = view === "dashboard" || view === "";
|
|
1446
|
+
const nav = buildNav(config, view, navigate, t2);
|
|
1447
|
+
const quickActions = React11.useMemo(() => {
|
|
1448
|
+
const actions = [
|
|
1449
|
+
{
|
|
1450
|
+
id: "new-product",
|
|
1451
|
+
label: t2("inventory.quickActions.newProduct"),
|
|
1452
|
+
icon: "Package",
|
|
1453
|
+
description: t2("inventory.quickActions.newProductDesc"),
|
|
1454
|
+
action: () => navigate("products-new")
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
id: "stock-entry",
|
|
1458
|
+
label: t2("inventory.quickActions.stockEntry"),
|
|
1459
|
+
icon: "ArrowUpRight",
|
|
1460
|
+
description: t2("inventory.quickActions.stockEntryDesc"),
|
|
1461
|
+
action: () => navigate("stock-entry")
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
id: "stock-exit",
|
|
1465
|
+
label: t2("inventory.quickActions.stockExit"),
|
|
1466
|
+
icon: "ArrowDownRight",
|
|
1467
|
+
description: t2("inventory.quickActions.stockExitDesc"),
|
|
1468
|
+
action: () => navigate("stock-exit")
|
|
1469
|
+
}
|
|
1470
|
+
];
|
|
1471
|
+
return actions;
|
|
1472
|
+
}, []);
|
|
1473
|
+
if (!onboardingComplete) {
|
|
1474
|
+
return /* @__PURE__ */ jsxRuntime.jsx(InventoryContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(InventoryOnboarding, { onComplete: () => {
|
|
1475
|
+
setOnboardingComplete(true);
|
|
1476
|
+
try {
|
|
1477
|
+
localStorage.setItem("saas-core:inventory-onboarded", "true");
|
|
1478
|
+
} catch {
|
|
1479
|
+
}
|
|
1480
|
+
} }) });
|
|
1481
|
+
}
|
|
1482
|
+
if (isSettings && registries && registries.length > 0) {
|
|
1483
|
+
return /* @__PURE__ */ jsxRuntime.jsx(InventoryContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ui.PageTransition, { transitionKey: "settings", direction, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "24px" }, children: [
|
|
1484
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginBottom: "16px" }, children: [
|
|
1485
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "20px", fontWeight: 600, margin: 0 }, children: t2("inventory.settingsPage.title") }),
|
|
1486
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "var(--muted-foreground, #6b7280)", margin: "4px 0 0", fontSize: "14px" }, children: t2("inventory.settingsPage.subtitle") })
|
|
1487
|
+
] }),
|
|
1488
|
+
/* @__PURE__ */ jsxRuntime.jsx(InventoryGeneralSettings, {})
|
|
1489
|
+
] }) }) });
|
|
1490
|
+
}
|
|
1491
|
+
const renderView = saas.createViewRouter([
|
|
1492
|
+
{ 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") }) },
|
|
1495
|
+
{ id: "stock-entry", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
|
|
1496
|
+
{ id: "stock-exit", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
|
|
1497
|
+
{ id: "stock-history", render: () => /* @__PURE__ */ jsxRuntime.jsx(MovementHistoryView, { onViewDetail: (id) => navigate(`stock-detail:${id}`) }) },
|
|
1498
|
+
{ id: "stock-detail", render: ({ id }) => {
|
|
1499
|
+
const movement = store.getState().movements.find((m) => m.id === id);
|
|
1500
|
+
return movement ? /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: movement.movementType, viewMovement: movement, onSaved: () => navigate("stock-history") }) : /* @__PURE__ */ jsxRuntime.jsx(MovementHistoryView, { onViewDetail: (mid) => navigate(`stock-detail:${mid}`) });
|
|
1501
|
+
} },
|
|
1502
|
+
{ id: "recipes-list", render: () => /* @__PURE__ */ jsxRuntime.jsx(RecipesView, { onNew: () => navigate("recipes-new"), onView: (id) => navigate(`recipes-detail:${id}`) }) },
|
|
1503
|
+
{ id: "recipes-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(RecipeFormView, { onSaved: (id) => id ? navigate(`recipes-detail:${id}`) : navigate("recipes-list") }) },
|
|
1504
|
+
{ id: "recipes-detail", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(RecipeDetailView, { recipeId: id, onBack: () => navigate("recipes-list") }) },
|
|
1505
|
+
{ id: "dashboard", render: () => /* @__PURE__ */ jsxRuntime.jsx(DashboardView, {}) }
|
|
1506
|
+
], "dashboard");
|
|
1507
|
+
return /* @__PURE__ */ jsxRuntime.jsx(InventoryContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1508
|
+
ui.ModulePage,
|
|
1509
|
+
{
|
|
1510
|
+
title: config.labels.pageTitle,
|
|
1511
|
+
subtitle: config.labels.pageSubtitle,
|
|
1512
|
+
nav,
|
|
1513
|
+
showHeader: isSummary,
|
|
1514
|
+
viewKey: view,
|
|
1515
|
+
direction,
|
|
1516
|
+
headerAction: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1517
|
+
saas.ModuleActionBar,
|
|
1518
|
+
{
|
|
1519
|
+
quickActions,
|
|
1520
|
+
settingsPath: registries && registries.length > 0 ? "/settings/inventory" : void 0,
|
|
1521
|
+
settingsLabel: "Inventory Settings"
|
|
1522
|
+
}
|
|
1523
|
+
),
|
|
1524
|
+
children: renderView(view)
|
|
1525
|
+
}
|
|
1526
|
+
) });
|
|
1527
|
+
}
|
|
1528
|
+
function useEnsureSummary() {
|
|
1529
|
+
const fetchSummary = useInventoryStore((s) => s.fetchSummary);
|
|
1530
|
+
React11.useEffect(() => {
|
|
1531
|
+
void fetchSummary();
|
|
1532
|
+
}, []);
|
|
1533
|
+
}
|
|
1534
|
+
function StockValueKpi() {
|
|
1535
|
+
const t2 = core.useTranslation();
|
|
1536
|
+
const { currency } = useInventoryConfig();
|
|
1537
|
+
const summary = useInventoryStore((s) => s.summary);
|
|
1538
|
+
useEnsureSummary();
|
|
1539
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1540
|
+
ui.KpiCard,
|
|
1541
|
+
{
|
|
1542
|
+
label: t2("inventory.dashboard.stockValue"),
|
|
1543
|
+
icon: "BarChart3",
|
|
1544
|
+
value: saas.formatCurrency(summary?.totalStockValue ?? 0, currency),
|
|
1545
|
+
sub: t2("inventory.dashboard.totalValue")
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
function TotalProductsKpi() {
|
|
1550
|
+
const t2 = core.useTranslation();
|
|
1551
|
+
const summary = useInventoryStore((s) => s.summary);
|
|
1552
|
+
useEnsureSummary();
|
|
1553
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.KpiCard, { label: t2("inventory.dashboard.totalProducts"), icon: "Package", value: String(summary?.totalProducts ?? 0), sub: t2("inventory.dashboard.activeItems") });
|
|
1554
|
+
}
|
|
1555
|
+
function LowStockKpi() {
|
|
1556
|
+
const t2 = core.useTranslation();
|
|
1557
|
+
const summary = useInventoryStore((s) => s.summary);
|
|
1558
|
+
useEnsureSummary();
|
|
1559
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.KpiCard, { label: t2("inventory.dashboard.lowStock"), icon: "AlertTriangle", value: String(summary?.lowStockCount ?? 0), sub: t2("inventory.dashboard.belowMinimum") });
|
|
1560
|
+
}
|
|
1561
|
+
function OutOfStockKpi() {
|
|
1562
|
+
const t2 = core.useTranslation();
|
|
1563
|
+
const summary = useInventoryStore((s) => s.summary);
|
|
1564
|
+
useEnsureSummary();
|
|
1565
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.KpiCard, { label: t2("inventory.dashboard.outOfStock"), icon: "TrendingDown", value: String(summary?.outOfStockCount ?? 0), sub: t2("inventory.dashboard.needRestocking") });
|
|
1566
|
+
}
|
|
1567
|
+
function RecentActivityPanel() {
|
|
1568
|
+
const t2 = core.useTranslation();
|
|
1569
|
+
const summary = useInventoryStore((s) => s.summary);
|
|
1570
|
+
useEnsureSummary();
|
|
1571
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { children: [
|
|
1572
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.CardHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.CardTitle, { children: t2("inventory.dashboard.recentActivity") }) }),
|
|
1573
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.CardContent, { children: [
|
|
1574
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-4 text-sm", children: [
|
|
1575
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1576
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUpRight, { className: "h-3.5 w-3.5 text-success" }),
|
|
1577
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: t2("inventory.dashboard.entries") }),
|
|
1578
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: summary?.movementsByType?.entry ?? 0 })
|
|
1579
|
+
] }),
|
|
1580
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1581
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowDownRight, { className: "h-3.5 w-3.5 text-destructive" }),
|
|
1582
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: t2("inventory.dashboard.exits") }),
|
|
1583
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: summary?.movementsByType?.exit ?? 0 })
|
|
1584
|
+
] })
|
|
1585
|
+
] }),
|
|
1586
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: t2("inventory.dashboard.last7Days") })
|
|
1587
|
+
] })
|
|
1588
|
+
] });
|
|
1589
|
+
}
|
|
1590
|
+
function createInventoryDashboardWidgets(ctx2) {
|
|
1591
|
+
const withCtx = (Inner) => {
|
|
1592
|
+
const Wrapped = () => /* @__PURE__ */ jsxRuntime.jsx(InventoryContextProvider, { config: ctx2.config, provider: ctx2.provider, store: ctx2.store, children: /* @__PURE__ */ jsxRuntime.jsx(Inner, {}) });
|
|
1593
|
+
Wrapped.displayName = `InventoryWidget(${Inner.displayName ?? Inner.name})`;
|
|
1594
|
+
return Wrapped;
|
|
1595
|
+
};
|
|
1596
|
+
return [
|
|
1597
|
+
// Stock value is inventory's headline KPI on the global home; the rest are
|
|
1598
|
+
// home-hidden by default (shown on the inventory plugin-home, addable via Customize).
|
|
1599
|
+
ui.defineKpiWidget({ id: "inventory.kpi.stock-value", title: "inventory.dashboard.stockValue", domain: "inventory", defaultOrder: 0, component: withCtx(StockValueKpi) }),
|
|
1600
|
+
ui.defineKpiWidget({ id: "inventory.kpi.total-products", title: "inventory.dashboard.totalProducts", domain: "inventory", defaultOrder: 1, defaultVisible: false, component: withCtx(TotalProductsKpi) }),
|
|
1601
|
+
ui.defineKpiWidget({ id: "inventory.kpi.low-stock", title: "inventory.dashboard.lowStock", domain: "inventory", defaultOrder: 2, defaultVisible: false, component: withCtx(LowStockKpi) }),
|
|
1602
|
+
ui.defineKpiWidget({ id: "inventory.kpi.out-of-stock", title: "inventory.dashboard.outOfStock", domain: "inventory", defaultOrder: 3, defaultVisible: false, component: withCtx(OutOfStockKpi) }),
|
|
1603
|
+
ui.defineCustomWidget({ id: "inventory.panel.recent-activity", title: "inventory.dashboard.recentActivity", domain: "inventory", span: 2, defaultOrder: 10, surfaces: ["plugin-home"], component: withCtx(RecentActivityPanel) })
|
|
1604
|
+
];
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/data/mock.ts
|
|
1608
|
+
var nextId2 = 1;
|
|
1609
|
+
function uid() {
|
|
1610
|
+
return String(nextId2++);
|
|
1611
|
+
}
|
|
1612
|
+
function now() {
|
|
1613
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1614
|
+
}
|
|
1615
|
+
function today() {
|
|
1616
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1617
|
+
}
|
|
1618
|
+
function paginate(items, page, pageSize) {
|
|
1619
|
+
const p = page ?? 1;
|
|
1620
|
+
const ps = pageSize ?? 50;
|
|
1621
|
+
const start = (p - 1) * ps;
|
|
1622
|
+
return { data: items.slice(start, start + ps), total: items.length };
|
|
1623
|
+
}
|
|
1624
|
+
function createStore() {
|
|
1625
|
+
return {
|
|
1626
|
+
products: [],
|
|
1627
|
+
movements: [],
|
|
1628
|
+
positions: [],
|
|
1629
|
+
locations: [
|
|
1630
|
+
{ id: uid(), name: "Main Storage", description: "Primary storage area", isActive: true, tenantId: "mock-tenant", createdAt: now(), updatedAt: now() }
|
|
1631
|
+
],
|
|
1632
|
+
recipes: [],
|
|
1633
|
+
recipeIngredients: []
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
function createMockInventoryProvider() {
|
|
1637
|
+
const store = createStore();
|
|
1638
|
+
const tenantId = "mock-tenant";
|
|
1639
|
+
const provider = {
|
|
1640
|
+
// --- Products ---
|
|
1641
|
+
async getProducts(query) {
|
|
1642
|
+
let results = [...store.products];
|
|
1643
|
+
if (query.productType) results = results.filter((p) => p.productType === query.productType);
|
|
1644
|
+
if (query.categoryId) results = results.filter((p) => p.categoryId === query.categoryId);
|
|
1645
|
+
if (query.isActive !== void 0) results = results.filter((p) => p.isActive === query.isActive);
|
|
1646
|
+
if (query.lowStockOnly) results = results.filter((p) => p.currentQuantity <= p.minQuantity);
|
|
1647
|
+
if (query.search) {
|
|
1648
|
+
const s = query.search.toLowerCase();
|
|
1649
|
+
results = results.filter((p) => p.name.toLowerCase().includes(s) || p.sku?.toLowerCase().includes(s) || p.barcode?.includes(s));
|
|
1650
|
+
}
|
|
1651
|
+
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
1652
|
+
return paginate(results, query.page, query.pageSize);
|
|
1653
|
+
},
|
|
1654
|
+
async getProductById(id) {
|
|
1655
|
+
return store.products.find((p) => p.id === id) ?? null;
|
|
1656
|
+
},
|
|
1657
|
+
async createProduct(input) {
|
|
1658
|
+
const product = {
|
|
1659
|
+
id: uid(),
|
|
1660
|
+
name: input.name,
|
|
1661
|
+
description: input.description,
|
|
1662
|
+
sku: input.sku,
|
|
1663
|
+
barcode: input.barcode,
|
|
1664
|
+
brand: input.brand,
|
|
1665
|
+
categoryId: input.categoryId,
|
|
1666
|
+
productType: input.productType,
|
|
1667
|
+
purpose: input.purpose,
|
|
1668
|
+
currentQuantity: 0,
|
|
1669
|
+
minQuantity: input.minQuantity ?? 0,
|
|
1670
|
+
maxQuantity: input.maxQuantity,
|
|
1671
|
+
costPrice: input.costPrice ?? 0,
|
|
1672
|
+
salePrice: input.salePrice,
|
|
1673
|
+
measurementUnitId: input.measurementUnitId,
|
|
1674
|
+
isActive: true,
|
|
1675
|
+
supplierId: input.supplierId,
|
|
1676
|
+
defaultLocationId: input.defaultLocationId,
|
|
1677
|
+
imageUrl: input.imageUrl,
|
|
1678
|
+
metadata: input.metadata,
|
|
1679
|
+
tenantId,
|
|
1680
|
+
createdAt: now(),
|
|
1681
|
+
updatedAt: now()
|
|
1682
|
+
};
|
|
1683
|
+
store.products.push(product);
|
|
1684
|
+
return product;
|
|
1685
|
+
},
|
|
1686
|
+
async updateProduct(id, data) {
|
|
1687
|
+
const product = store.products.find((p) => p.id === id);
|
|
1688
|
+
if (!product) throw new Error(`Product ${id} not found`);
|
|
1689
|
+
Object.assign(product, data, { updatedAt: now() });
|
|
1690
|
+
return product;
|
|
1691
|
+
},
|
|
1692
|
+
// --- Stock Movements ---
|
|
1693
|
+
async getMovements(query) {
|
|
1694
|
+
let results = [...store.movements];
|
|
1695
|
+
if (query.productId) results = results.filter((m) => m.productId === query.productId);
|
|
1696
|
+
if (query.movementType) {
|
|
1697
|
+
const types = Array.isArray(query.movementType) ? query.movementType : [query.movementType];
|
|
1698
|
+
results = results.filter((m) => types.includes(m.movementType));
|
|
1699
|
+
}
|
|
1700
|
+
if (query.stockLocationId) results = results.filter((m) => m.stockLocationId === query.stockLocationId);
|
|
1701
|
+
if (query.dateRange) {
|
|
1702
|
+
results = results.filter((m) => m.movementDate >= query.dateRange.from && m.movementDate <= query.dateRange.to);
|
|
1703
|
+
}
|
|
1704
|
+
if (query.search) {
|
|
1705
|
+
const s = query.search.toLowerCase();
|
|
1706
|
+
results = results.filter((m) => m.productName?.toLowerCase().includes(s) || m.notes?.toLowerCase().includes(s));
|
|
1707
|
+
}
|
|
1708
|
+
results.sort((a, b) => b.movementDate.localeCompare(a.movementDate));
|
|
1709
|
+
return paginate(results, query.page, query.pageSize);
|
|
1710
|
+
},
|
|
1711
|
+
async createMovement(input) {
|
|
1712
|
+
const product = store.products.find((p) => p.id === input.productId);
|
|
1713
|
+
const location = input.stockLocationId ? store.locations.find((l) => l.id === input.stockLocationId) : void 0;
|
|
1714
|
+
const unitCost = input.unitCost ?? product?.costPrice ?? 0;
|
|
1715
|
+
const movement = {
|
|
1716
|
+
id: uid(),
|
|
1717
|
+
productId: input.productId,
|
|
1718
|
+
productName: product?.name,
|
|
1719
|
+
quantity: input.quantity,
|
|
1720
|
+
movementType: input.movementType,
|
|
1721
|
+
unitCost,
|
|
1722
|
+
totalCost: unitCost * input.quantity,
|
|
1723
|
+
stockLocationId: input.stockLocationId,
|
|
1724
|
+
stockLocationName: location?.name,
|
|
1725
|
+
destinationLocationId: input.destinationLocationId,
|
|
1726
|
+
supplierId: input.supplierId,
|
|
1727
|
+
documentNumber: input.documentNumber,
|
|
1728
|
+
reason: input.reason,
|
|
1729
|
+
notes: input.notes,
|
|
1730
|
+
movementDate: input.movementDate ?? today(),
|
|
1731
|
+
tenantId,
|
|
1732
|
+
createdAt: now()
|
|
1733
|
+
};
|
|
1734
|
+
store.movements.push(movement);
|
|
1735
|
+
if (product) {
|
|
1736
|
+
if (input.movementType === "entry") product.currentQuantity += input.quantity;
|
|
1737
|
+
else if (input.movementType === "exit" || input.movementType === "loss") product.currentQuantity -= input.quantity;
|
|
1738
|
+
product.updatedAt = now();
|
|
1739
|
+
}
|
|
1740
|
+
return movement;
|
|
1741
|
+
},
|
|
1742
|
+
// --- Stock Positions ---
|
|
1743
|
+
async getPositions(productId) {
|
|
1744
|
+
return store.positions.filter((p) => p.productId === productId);
|
|
1745
|
+
},
|
|
1746
|
+
// --- Stock Locations ---
|
|
1747
|
+
async getLocations() {
|
|
1748
|
+
return store.locations.filter((l) => l.isActive);
|
|
1749
|
+
},
|
|
1750
|
+
async createLocation(data) {
|
|
1751
|
+
const location = {
|
|
1752
|
+
id: uid(),
|
|
1753
|
+
name: data.name,
|
|
1754
|
+
description: data.description,
|
|
1755
|
+
isActive: true,
|
|
1756
|
+
unitId: data.unitId,
|
|
1757
|
+
tenantId,
|
|
1758
|
+
createdAt: now(),
|
|
1759
|
+
updatedAt: now()
|
|
1760
|
+
};
|
|
1761
|
+
store.locations.push(location);
|
|
1762
|
+
return location;
|
|
1763
|
+
},
|
|
1764
|
+
// --- Recipes ---
|
|
1765
|
+
async getRecipes() {
|
|
1766
|
+
return store.recipes.filter((r) => r.isActive);
|
|
1767
|
+
},
|
|
1768
|
+
async getRecipeById(id) {
|
|
1769
|
+
return store.recipes.find((r) => r.id === id) ?? null;
|
|
1770
|
+
},
|
|
1771
|
+
async getRecipeIngredients(recipeId) {
|
|
1772
|
+
return store.recipeIngredients.filter((ri) => ri.recipeId === recipeId).sort((a, b) => a.displayOrder - b.displayOrder);
|
|
1773
|
+
},
|
|
1774
|
+
async createRecipe(input) {
|
|
1775
|
+
const product = store.products.find((p) => p.id === input.productId);
|
|
1776
|
+
const recipeId = uid();
|
|
1777
|
+
const recipe = {
|
|
1778
|
+
id: recipeId,
|
|
1779
|
+
name: input.name,
|
|
1780
|
+
description: input.description,
|
|
1781
|
+
productId: input.productId,
|
|
1782
|
+
productName: product?.name,
|
|
1783
|
+
yieldQuantity: input.yieldQuantity,
|
|
1784
|
+
yieldUnitId: input.yieldUnitId,
|
|
1785
|
+
preparationTimeMinutes: input.preparationTimeMinutes,
|
|
1786
|
+
instructions: input.instructions,
|
|
1787
|
+
isActive: true,
|
|
1788
|
+
ingredientCount: input.ingredients.length,
|
|
1789
|
+
tenantId,
|
|
1790
|
+
createdAt: now(),
|
|
1791
|
+
updatedAt: now()
|
|
1792
|
+
};
|
|
1793
|
+
store.recipes.push(recipe);
|
|
1794
|
+
for (const ing of input.ingredients) {
|
|
1795
|
+
const ingProduct = store.products.find((p) => p.id === ing.productId);
|
|
1796
|
+
store.recipeIngredients.push({
|
|
1797
|
+
id: uid(),
|
|
1798
|
+
recipeId,
|
|
1799
|
+
productId: ing.productId,
|
|
1800
|
+
productName: ingProduct?.name,
|
|
1801
|
+
quantity: ing.quantity,
|
|
1802
|
+
unitId: ing.unitId,
|
|
1803
|
+
displayOrder: ing.displayOrder ?? 0,
|
|
1804
|
+
notes: ing.notes,
|
|
1805
|
+
createdAt: now()
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
return recipe;
|
|
1809
|
+
},
|
|
1810
|
+
// --- Summary ---
|
|
1811
|
+
async getSummary() {
|
|
1812
|
+
const active = store.products.filter((p) => p.isActive);
|
|
1813
|
+
const lowStock = active.filter((p) => p.currentQuantity <= p.minQuantity && p.currentQuantity > 0);
|
|
1814
|
+
const outOfStock = active.filter((p) => p.currentQuantity <= 0);
|
|
1815
|
+
const totalValue = active.reduce((sum, p) => sum + p.currentQuantity * p.costPrice, 0);
|
|
1816
|
+
const weekAgo = /* @__PURE__ */ new Date();
|
|
1817
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
1818
|
+
const weekAgoStr = weekAgo.toISOString().slice(0, 10);
|
|
1819
|
+
const recent = store.movements.filter((m) => m.movementDate >= weekAgoStr);
|
|
1820
|
+
const movementsByType = { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 };
|
|
1821
|
+
for (const m of recent) movementsByType[m.movementType]++;
|
|
1822
|
+
return {
|
|
1823
|
+
totalProducts: active.length,
|
|
1824
|
+
lowStockCount: lowStock.length,
|
|
1825
|
+
outOfStockCount: outOfStock.length,
|
|
1826
|
+
totalStockValue: totalValue,
|
|
1827
|
+
recentMovementCount: recent.length,
|
|
1828
|
+
movementsByType
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
return provider;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// src/data/supabase.ts
|
|
1836
|
+
function getTenantId() {
|
|
1837
|
+
return core.getActiveTenantId();
|
|
1838
|
+
}
|
|
1839
|
+
function snakeToCamel(obj) {
|
|
1840
|
+
const result = {};
|
|
1841
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1842
|
+
const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1843
|
+
result[camelKey] = value;
|
|
1844
|
+
}
|
|
1845
|
+
return result;
|
|
1846
|
+
}
|
|
1847
|
+
function camelToSnake(obj) {
|
|
1848
|
+
const result = {};
|
|
1849
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1850
|
+
if (key.startsWith("_")) continue;
|
|
1851
|
+
const snakeKey = key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
1852
|
+
result[snakeKey] = value;
|
|
1853
|
+
}
|
|
1854
|
+
return result;
|
|
1855
|
+
}
|
|
1856
|
+
function mapProductRow(row) {
|
|
1857
|
+
const meta = row.metadata ?? {};
|
|
1858
|
+
return {
|
|
1859
|
+
id: row.id,
|
|
1860
|
+
name: row.name,
|
|
1861
|
+
description: row.description,
|
|
1862
|
+
sku: row.sku,
|
|
1863
|
+
barcode: meta.barcode,
|
|
1864
|
+
brand: meta.brand,
|
|
1865
|
+
productType: meta.productType ?? "sale",
|
|
1866
|
+
currentQuantity: row.stock ?? 0,
|
|
1867
|
+
minQuantity: row.min_stock ?? 0,
|
|
1868
|
+
maxQuantity: meta.maxQuantity,
|
|
1869
|
+
costPrice: row.cost ?? 0,
|
|
1870
|
+
salePrice: row.price,
|
|
1871
|
+
isActive: row.is_active ?? true,
|
|
1872
|
+
imageUrl: row.image_url,
|
|
1873
|
+
metadata: meta,
|
|
1874
|
+
tenantId: row.tenant_id,
|
|
1875
|
+
createdAt: row.created_at,
|
|
1876
|
+
updatedAt: row.updated_at
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function createSupabaseInventoryProvider() {
|
|
1880
|
+
function getClients() {
|
|
1881
|
+
const supabase = core.getSupabaseClientOptional();
|
|
1882
|
+
if (!supabase) throw new Error("Supabase not initialized");
|
|
1883
|
+
return { core: supabase.schema("saas_core"), pub: supabase };
|
|
1884
|
+
}
|
|
1885
|
+
const provider = {
|
|
1886
|
+
// --- Products (saas_core.products) ---
|
|
1887
|
+
async getProducts(query) {
|
|
1888
|
+
const { core} = getClients();
|
|
1889
|
+
let qb = core.from("products").select("*", { count: "exact" });
|
|
1890
|
+
if (query.search) qb = qb.ilike("name", `%${query.search}%`);
|
|
1891
|
+
if (query.isActive !== void 0) qb = qb.eq("is_active", query.isActive);
|
|
1892
|
+
if (query.lowStockOnly) qb = qb.lte("stock", 0);
|
|
1893
|
+
const page = query.page ?? 1;
|
|
1894
|
+
const pageSize = query.pageSize ?? 50;
|
|
1895
|
+
qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("created_at", { ascending: false });
|
|
1896
|
+
const { data, count } = await qb;
|
|
1897
|
+
return { data: (data ?? []).map(mapProductRow), total: count ?? 0 };
|
|
1898
|
+
},
|
|
1899
|
+
async getProductById(id) {
|
|
1900
|
+
const { core } = getClients();
|
|
1901
|
+
const { data } = await core.from("products").select("*").eq("id", id).single();
|
|
1902
|
+
return data ? mapProductRow(data) : null;
|
|
1903
|
+
},
|
|
1904
|
+
async createProduct(input) {
|
|
1905
|
+
const { core } = getClients();
|
|
1906
|
+
const tenantId = getTenantId();
|
|
1907
|
+
const row = {
|
|
1908
|
+
tenant_id: tenantId,
|
|
1909
|
+
name: input.name,
|
|
1910
|
+
description: input.description,
|
|
1911
|
+
sku: input.sku,
|
|
1912
|
+
price: input.salePrice ?? 0,
|
|
1913
|
+
cost: input.costPrice ?? 0,
|
|
1914
|
+
stock: 0,
|
|
1915
|
+
min_stock: input.minQuantity ?? 0,
|
|
1916
|
+
is_active: true,
|
|
1917
|
+
image_url: input.imageUrl,
|
|
1918
|
+
metadata: { productType: input.productType, barcode: input.barcode, brand: input.brand, maxQuantity: input.maxQuantity }
|
|
1919
|
+
};
|
|
1920
|
+
const { data, error } = await core.from("products").insert(row).select().single();
|
|
1921
|
+
if (error) throw new Error(error.message);
|
|
1922
|
+
return mapProductRow(data);
|
|
1923
|
+
},
|
|
1924
|
+
async updateProduct(id, partial) {
|
|
1925
|
+
const { core } = getClients();
|
|
1926
|
+
const row = {};
|
|
1927
|
+
if (partial.name !== void 0) row.name = partial.name;
|
|
1928
|
+
if (partial.description !== void 0) row.description = partial.description;
|
|
1929
|
+
if (partial.sku !== void 0) row.sku = partial.sku;
|
|
1930
|
+
if (partial.salePrice !== void 0) row.price = partial.salePrice;
|
|
1931
|
+
if (partial.costPrice !== void 0) row.cost = partial.costPrice;
|
|
1932
|
+
if (partial.minQuantity !== void 0) row.min_stock = partial.minQuantity;
|
|
1933
|
+
if (partial.isActive !== void 0) row.is_active = partial.isActive;
|
|
1934
|
+
if (partial.imageUrl !== void 0) row.image_url = partial.imageUrl;
|
|
1935
|
+
if (partial.productType !== void 0 || partial.barcode !== void 0 || partial.brand !== void 0) {
|
|
1936
|
+
row.metadata = { productType: partial.productType, barcode: partial.barcode, brand: partial.brand };
|
|
1937
|
+
}
|
|
1938
|
+
const { data, error } = await core.from("products").update(row).eq("id", id).select().single();
|
|
1939
|
+
if (error) throw new Error(error.message);
|
|
1940
|
+
return mapProductRow(data);
|
|
1941
|
+
},
|
|
1942
|
+
// --- Stock Movements (via view with product join — single query) ---
|
|
1943
|
+
async getMovements(query) {
|
|
1944
|
+
const { pub } = getClients();
|
|
1945
|
+
let qb = pub.from("v_stock_movements").select("*", { count: "exact" });
|
|
1946
|
+
if (query.productId) qb = qb.eq("product_id", query.productId);
|
|
1947
|
+
if (query.movementType) {
|
|
1948
|
+
const types = Array.isArray(query.movementType) ? query.movementType : [query.movementType];
|
|
1949
|
+
qb = qb.in("movement_type", types);
|
|
1950
|
+
}
|
|
1951
|
+
if (query.stockLocationId) qb = qb.eq("stock_location_id", query.stockLocationId);
|
|
1952
|
+
if (query.dateRange) qb = qb.gte("movement_date", query.dateRange.from).lte("movement_date", query.dateRange.to);
|
|
1953
|
+
if (query.search) qb = qb.ilike("product_name", `%${query.search}%`);
|
|
1954
|
+
const page = query.page ?? 1;
|
|
1955
|
+
const pageSize = query.pageSize ?? 50;
|
|
1956
|
+
qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("movement_date", { ascending: false });
|
|
1957
|
+
const { data, count } = await qb;
|
|
1958
|
+
const movements = (data ?? []).map((r) => {
|
|
1959
|
+
const mov = snakeToCamel(r);
|
|
1960
|
+
mov.productName = r.product_name ?? mov.productName;
|
|
1961
|
+
mov.productSku = r.product_sku ?? mov.productSku;
|
|
1962
|
+
mov.stockLocationName = r.stock_location_name ?? mov.stockLocationName;
|
|
1963
|
+
mov.destinationLocationName = r.destination_location_name ?? mov.destinationLocationName;
|
|
1964
|
+
return mov;
|
|
1965
|
+
});
|
|
1966
|
+
const needsLocationResolve = movements.some(
|
|
1967
|
+
(m) => m.stockLocationId && !m.stockLocationName || m.destinationLocationId && !m.destinationLocationName
|
|
1968
|
+
);
|
|
1969
|
+
if (needsLocationResolve) {
|
|
1970
|
+
const locationIds = /* @__PURE__ */ new Set();
|
|
1971
|
+
for (const m of movements) {
|
|
1972
|
+
if (m.stockLocationId && !m.stockLocationName) locationIds.add(m.stockLocationId);
|
|
1973
|
+
if (m.destinationLocationId && !m.destinationLocationName) locationIds.add(m.destinationLocationId);
|
|
1974
|
+
}
|
|
1975
|
+
if (locationIds.size > 0) {
|
|
1976
|
+
const { data: locs } = await pub.from("stock_locations").select("id, name").in("id", [...locationIds]);
|
|
1977
|
+
const locMap = new Map((locs ?? []).map((l) => [l.id, l.name]));
|
|
1978
|
+
for (const m of movements) {
|
|
1979
|
+
if (m.stockLocationId && !m.stockLocationName) m.stockLocationName = locMap.get(m.stockLocationId);
|
|
1980
|
+
if (m.destinationLocationId && !m.destinationLocationName) m.destinationLocationName = locMap.get(m.destinationLocationId);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return { data: movements, total: count ?? 0 };
|
|
1985
|
+
},
|
|
1986
|
+
async createMovement(input) {
|
|
1987
|
+
const { core, pub } = getClients();
|
|
1988
|
+
const tenantId = getTenantId();
|
|
1989
|
+
const unitCost = input.unitCost ?? 0;
|
|
1990
|
+
const row = {
|
|
1991
|
+
...camelToSnake(input),
|
|
1992
|
+
tenant_id: tenantId,
|
|
1993
|
+
unit_cost: unitCost,
|
|
1994
|
+
total_cost: unitCost * input.quantity,
|
|
1995
|
+
movement_date: input.movementDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1996
|
+
};
|
|
1997
|
+
const { data, error } = await pub.from("stock_movements").insert(row).select().single();
|
|
1998
|
+
if (error) throw new Error(error.message);
|
|
1999
|
+
const { data: product } = await core.from("products").select("id, name, stock").eq("id", input.productId).single();
|
|
2000
|
+
if (product) {
|
|
2001
|
+
const delta = input.movementType === "entry" ? input.quantity : -input.quantity;
|
|
2002
|
+
if (input.movementType !== "adjustment" && input.movementType !== "transfer") {
|
|
2003
|
+
await core.from("products").update({ stock: (product.stock ?? 0) + delta }).eq("id", input.productId);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (input.stockLocationId) {
|
|
2007
|
+
try {
|
|
2008
|
+
const posDelta = input.movementType === "entry" ? input.quantity : -input.quantity;
|
|
2009
|
+
const { data: existing } = await pub.from("stock_positions").select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.stockLocationId).maybeSingle();
|
|
2010
|
+
if (existing) {
|
|
2011
|
+
await pub.from("stock_positions").update({ quantity: (existing.quantity ?? 0) + posDelta, unit_cost: input.unitCost ?? 0 }).eq("id", existing.id);
|
|
2012
|
+
} else {
|
|
2013
|
+
await pub.from("stock_positions").insert({
|
|
2014
|
+
tenant_id: tenantId,
|
|
2015
|
+
product_id: input.productId,
|
|
2016
|
+
stock_location_id: input.stockLocationId,
|
|
2017
|
+
quantity: Math.max(0, posDelta),
|
|
2018
|
+
unit_cost: input.unitCost ?? 0,
|
|
2019
|
+
batch_number: input.batchNumber ?? null,
|
|
2020
|
+
expiration_date: input.expirationDate ?? null
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
if (input.movementType === "transfer" && input.destinationLocationId) {
|
|
2024
|
+
const { data: destExisting } = await pub.from("stock_positions").select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.destinationLocationId).maybeSingle();
|
|
2025
|
+
if (destExisting) {
|
|
2026
|
+
await pub.from("stock_positions").update({ quantity: (destExisting.quantity ?? 0) + input.quantity, unit_cost: input.unitCost ?? 0 }).eq("id", destExisting.id);
|
|
2027
|
+
} else {
|
|
2028
|
+
await pub.from("stock_positions").insert({
|
|
2029
|
+
tenant_id: tenantId,
|
|
2030
|
+
product_id: input.productId,
|
|
2031
|
+
stock_location_id: input.destinationLocationId,
|
|
2032
|
+
quantity: input.quantity,
|
|
2033
|
+
unit_cost: input.unitCost ?? 0,
|
|
2034
|
+
batch_number: input.batchNumber ?? null,
|
|
2035
|
+
expiration_date: input.expirationDate ?? null
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
} catch {
|
|
2040
|
+
console.warn("Failed to update stock_positions \u2014 table may not exist yet");
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const movement = snakeToCamel(data);
|
|
2044
|
+
movement.productName = product?.name;
|
|
2045
|
+
if (input.stockLocationId) {
|
|
2046
|
+
const { data: loc } = await pub.from("stock_locations").select("name").eq("id", input.stockLocationId).single();
|
|
2047
|
+
movement.stockLocationName = loc?.name;
|
|
2048
|
+
}
|
|
2049
|
+
if (input.destinationLocationId) {
|
|
2050
|
+
const { data: loc } = await pub.from("stock_locations").select("name").eq("id", input.destinationLocationId).single();
|
|
2051
|
+
movement.destinationLocationName = loc?.name;
|
|
2052
|
+
}
|
|
2053
|
+
return movement;
|
|
2054
|
+
},
|
|
2055
|
+
// --- Stock Positions ---
|
|
2056
|
+
async getPositions(productId) {
|
|
2057
|
+
const { pub } = getClients();
|
|
2058
|
+
const { data } = await pub.from("stock_positions").select("*").eq("product_id", productId);
|
|
2059
|
+
return (data ?? []).map((r) => snakeToCamel(r));
|
|
2060
|
+
},
|
|
2061
|
+
// --- Stock Locations ---
|
|
2062
|
+
async getLocations() {
|
|
2063
|
+
const { pub } = getClients();
|
|
2064
|
+
const { data } = await pub.from("stock_locations").select("*").eq("is_active", true).order("name");
|
|
2065
|
+
return (data ?? []).map((r) => snakeToCamel(r));
|
|
2066
|
+
},
|
|
2067
|
+
async createLocation(input) {
|
|
2068
|
+
const { pub } = getClients();
|
|
2069
|
+
const tenantId = getTenantId();
|
|
2070
|
+
const { data } = await pub.from("stock_locations").insert({ ...camelToSnake(input), tenant_id: tenantId }).select().single();
|
|
2071
|
+
return snakeToCamel(data);
|
|
2072
|
+
},
|
|
2073
|
+
// --- Recipes ---
|
|
2074
|
+
async getRecipes() {
|
|
2075
|
+
const { pub } = getClients();
|
|
2076
|
+
const { data } = await pub.from("recipes").select("*").eq("is_active", true).order("name");
|
|
2077
|
+
return (data ?? []).map((r) => snakeToCamel(r));
|
|
2078
|
+
},
|
|
2079
|
+
async getRecipeById(id) {
|
|
2080
|
+
const { pub } = getClients();
|
|
2081
|
+
const { data } = await pub.from("recipes").select("*").eq("id", id).single();
|
|
2082
|
+
return data ? snakeToCamel(data) : null;
|
|
2083
|
+
},
|
|
2084
|
+
async getRecipeIngredients(recipeId) {
|
|
2085
|
+
const { pub } = getClients();
|
|
2086
|
+
const { data } = await pub.from("recipe_ingredients").select("*").eq("recipe_id", recipeId).order("display_order");
|
|
2087
|
+
return (data ?? []).map((r) => snakeToCamel(r));
|
|
2088
|
+
},
|
|
2089
|
+
async createRecipe(input) {
|
|
2090
|
+
const { pub } = getClients();
|
|
2091
|
+
const tenantId = getTenantId();
|
|
2092
|
+
const { ingredients, ...recipeData } = input;
|
|
2093
|
+
const { data: recipe } = await pub.from("recipes").insert({ ...camelToSnake(recipeData), tenant_id: tenantId }).select().single();
|
|
2094
|
+
if (recipe && ingredients.length > 0) {
|
|
2095
|
+
await pub.from("recipe_ingredients").insert(
|
|
2096
|
+
ingredients.map((ing) => ({ ...camelToSnake(ing), recipe_id: recipe.id, tenant_id: tenantId }))
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
return snakeToCamel(recipe);
|
|
2100
|
+
},
|
|
2101
|
+
// --- Summary ---
|
|
2102
|
+
async getSummary() {
|
|
2103
|
+
const { core, pub } = getClients();
|
|
2104
|
+
const { data: products } = await core.from("products").select("stock, min_stock, price, is_active").eq("is_active", true);
|
|
2105
|
+
const items = products ?? [];
|
|
2106
|
+
const lowStock = items.filter((p) => p.stock > 0 && p.stock <= (p.min_stock ?? 0));
|
|
2107
|
+
const outOfStock = items.filter((p) => p.stock <= 0);
|
|
2108
|
+
const totalValue = items.reduce((sum, p) => sum + (p.stock ?? 0) * (p.price ?? 0), 0);
|
|
2109
|
+
const weekAgo = /* @__PURE__ */ new Date();
|
|
2110
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
2111
|
+
const { data: movements } = await pub.from("stock_movements").select("movement_type").gte("movement_date", weekAgo.toISOString().slice(0, 10));
|
|
2112
|
+
const movs = movements ?? [];
|
|
2113
|
+
const movementsByType = { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 };
|
|
2114
|
+
for (const m of movs) movementsByType[m.movement_type]++;
|
|
2115
|
+
return {
|
|
2116
|
+
totalProducts: items.length,
|
|
2117
|
+
lowStockCount: lowStock.length,
|
|
2118
|
+
outOfStockCount: outOfStock.length,
|
|
2119
|
+
totalStockValue: totalValue,
|
|
2120
|
+
recentMovementCount: movs.length,
|
|
2121
|
+
movementsByType
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
return provider;
|
|
2126
|
+
}
|
|
2127
|
+
function createInventoryStore(provider) {
|
|
2128
|
+
return vanilla.createStore((set, get) => ({
|
|
2129
|
+
products: [],
|
|
2130
|
+
productsTotal: 0,
|
|
2131
|
+
productsLoading: false,
|
|
2132
|
+
productQuery: {},
|
|
2133
|
+
movements: [],
|
|
2134
|
+
movementsTotal: 0,
|
|
2135
|
+
movementsLoading: false,
|
|
2136
|
+
locations: [],
|
|
2137
|
+
locationsLoading: false,
|
|
2138
|
+
recipes: [],
|
|
2139
|
+
recipesLoading: false,
|
|
2140
|
+
summary: null,
|
|
2141
|
+
summaryLoading: false,
|
|
2142
|
+
async fetchSummary() {
|
|
2143
|
+
return saas.dedup("inv:summary", async () => {
|
|
2144
|
+
set({ summaryLoading: true });
|
|
2145
|
+
const summary = await provider.getSummary();
|
|
2146
|
+
set({ summary, summaryLoading: false });
|
|
2147
|
+
});
|
|
2148
|
+
},
|
|
2149
|
+
async fetchProducts(query) {
|
|
2150
|
+
const key = "inv:products:" + JSON.stringify(query);
|
|
2151
|
+
return saas.dedup(key, async () => {
|
|
2152
|
+
set({ productsLoading: true, productQuery: query });
|
|
2153
|
+
const result = await provider.getProducts(query);
|
|
2154
|
+
set({ products: result.data, productsTotal: result.total, productsLoading: false });
|
|
2155
|
+
});
|
|
2156
|
+
},
|
|
2157
|
+
async fetchMovements(query) {
|
|
2158
|
+
return saas.dedup("inv:movements:" + JSON.stringify(query), async () => {
|
|
2159
|
+
set({ movementsLoading: true });
|
|
2160
|
+
const result = await provider.getMovements(query);
|
|
2161
|
+
set({ movements: result.data, movementsTotal: result.total, movementsLoading: false });
|
|
2162
|
+
});
|
|
2163
|
+
},
|
|
2164
|
+
async fetchLocations() {
|
|
2165
|
+
return saas.dedup("inv:locations", async () => {
|
|
2166
|
+
set({ locationsLoading: true });
|
|
2167
|
+
const locations = await provider.getLocations();
|
|
2168
|
+
set({ locations, locationsLoading: false });
|
|
2169
|
+
});
|
|
2170
|
+
},
|
|
2171
|
+
async fetchRecipes() {
|
|
2172
|
+
set({ recipesLoading: true });
|
|
2173
|
+
const recipes = await provider.getRecipes();
|
|
2174
|
+
set({ recipes, recipesLoading: false });
|
|
2175
|
+
},
|
|
2176
|
+
async createProduct(input) {
|
|
2177
|
+
try {
|
|
2178
|
+
const product = await provider.createProduct(input);
|
|
2179
|
+
const query = get().productQuery;
|
|
2180
|
+
const [result, summary] = await Promise.all([provider.getProducts(query), provider.getSummary()]);
|
|
2181
|
+
set({ products: result.data, productsTotal: result.total, summary });
|
|
2182
|
+
ui.toast.success("Product created");
|
|
2183
|
+
return product;
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
ui.toast.error("Failed to create product", { description: err?.message });
|
|
2186
|
+
throw err;
|
|
2187
|
+
}
|
|
2188
|
+
},
|
|
2189
|
+
async createMovement(input) {
|
|
2190
|
+
try {
|
|
2191
|
+
const movement = await provider.createMovement(input);
|
|
2192
|
+
const [summary] = await Promise.all([provider.getSummary()]);
|
|
2193
|
+
set({ summary });
|
|
2194
|
+
ui.toast.success("Stock movement recorded");
|
|
2195
|
+
return movement;
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
ui.toast.error("Failed to record movement", { description: err?.message });
|
|
2198
|
+
throw err;
|
|
2199
|
+
}
|
|
2200
|
+
},
|
|
2201
|
+
async createRecipe(input) {
|
|
2202
|
+
try {
|
|
2203
|
+
const recipe = await provider.createRecipe(input);
|
|
2204
|
+
const recipes = await provider.getRecipes();
|
|
2205
|
+
set({ recipes });
|
|
2206
|
+
ui.toast.success("Recipe created");
|
|
2207
|
+
return recipe;
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
ui.toast.error("Failed to create recipe", { description: err?.message });
|
|
2210
|
+
throw err;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}));
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/registries.ts
|
|
2217
|
+
var supplierEntity = {
|
|
2218
|
+
name: "Supplier",
|
|
2219
|
+
namePlural: "Suppliers",
|
|
2220
|
+
icon: "Building2",
|
|
2221
|
+
layout: "person",
|
|
2222
|
+
displayField: "name",
|
|
2223
|
+
defaultSort: "name",
|
|
2224
|
+
fields: [
|
|
2225
|
+
{ key: "name", label: "Name", type: "text", required: true, showInTable: true, searchable: true },
|
|
2226
|
+
{ key: "phone", label: "Phone", type: "phone", showInTable: true },
|
|
2227
|
+
{ key: "email", label: "Email", type: "email", showInTable: true },
|
|
2228
|
+
{ key: "documentNumber", label: "Tax ID", type: "text", showInTable: true },
|
|
2229
|
+
{ key: "address", label: "Address", type: "text" },
|
|
2230
|
+
{ key: "notes", label: "Notes", type: "textarea" },
|
|
2231
|
+
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2232
|
+
],
|
|
2233
|
+
data: {
|
|
2234
|
+
table: "persons",
|
|
2235
|
+
schema: "saas_core",
|
|
2236
|
+
tenantScoped: true,
|
|
2237
|
+
filters: { kind: "supplier" },
|
|
2238
|
+
defaults: { kind: "supplier" }
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
var measurementUnitEntity = {
|
|
2242
|
+
name: "Measurement Unit",
|
|
2243
|
+
namePlural: "Measurement Units",
|
|
2244
|
+
icon: "Ruler",
|
|
2245
|
+
displayField: "name",
|
|
2246
|
+
defaultSort: "name",
|
|
2247
|
+
fields: [
|
|
2248
|
+
{ key: "name", label: "Name", type: "text", required: true, showInTable: true },
|
|
2249
|
+
{ key: "abbreviation", label: "Abbreviation", type: "text", required: true, showInTable: true },
|
|
2250
|
+
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2251
|
+
],
|
|
2252
|
+
data: { table: "measurement_units", tenantScoped: true }
|
|
2253
|
+
};
|
|
2254
|
+
var productCategoryEntity = {
|
|
2255
|
+
name: "Product Category",
|
|
2256
|
+
namePlural: "Product Categories",
|
|
2257
|
+
icon: "Tag",
|
|
2258
|
+
displayField: "name",
|
|
2259
|
+
defaultSort: "name",
|
|
2260
|
+
fields: [
|
|
2261
|
+
{ key: "name", label: "Name", type: "text", required: true, showInTable: true },
|
|
2262
|
+
{ key: "parentId", label: "Parent", type: "text", showInTable: false },
|
|
2263
|
+
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2264
|
+
],
|
|
2265
|
+
data: { table: "product_categories", tenantScoped: true }
|
|
2266
|
+
};
|
|
2267
|
+
var stockLocationEntity = {
|
|
2268
|
+
name: "Stock Location",
|
|
2269
|
+
namePlural: "Stock Locations",
|
|
2270
|
+
icon: "Warehouse",
|
|
2271
|
+
displayField: "name",
|
|
2272
|
+
defaultSort: "name",
|
|
2273
|
+
fields: [
|
|
2274
|
+
{ key: "name", label: "Name", type: "text", required: true, showInTable: true },
|
|
2275
|
+
{ key: "description", label: "Description", type: "textarea", showInTable: true },
|
|
2276
|
+
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2277
|
+
],
|
|
2278
|
+
data: { table: "stock_locations", tenantScoped: true }
|
|
2279
|
+
};
|
|
2280
|
+
var inventoryRegistries = [
|
|
2281
|
+
// --- Editable ---
|
|
2282
|
+
{
|
|
2283
|
+
id: "suppliers",
|
|
2284
|
+
entity: supplierEntity,
|
|
2285
|
+
icon: "Building2",
|
|
2286
|
+
description: "Manage your product and material suppliers",
|
|
2287
|
+
display: "table"
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
id: "product-categories",
|
|
2291
|
+
entity: productCategoryEntity,
|
|
2292
|
+
icon: "Tag",
|
|
2293
|
+
description: "Product categories for organization",
|
|
2294
|
+
display: "table"
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
id: "stock-locations",
|
|
2298
|
+
entity: stockLocationEntity,
|
|
2299
|
+
icon: "Warehouse",
|
|
2300
|
+
description: "Physical storage locations for inventory",
|
|
2301
|
+
display: "table"
|
|
2302
|
+
},
|
|
2303
|
+
// --- Read-only (system / seeded) ---
|
|
2304
|
+
{
|
|
2305
|
+
id: "measurement-units",
|
|
2306
|
+
entity: measurementUnitEntity,
|
|
2307
|
+
icon: "Ruler",
|
|
2308
|
+
description: "Standard units of measure",
|
|
2309
|
+
display: "table",
|
|
2310
|
+
readOnly: true,
|
|
2311
|
+
seedData: [
|
|
2312
|
+
{ id: "mu-unit", name: "Unit", abbreviation: "un", isActive: true },
|
|
2313
|
+
{ id: "mu-box", name: "Box", abbreviation: "box", isActive: true },
|
|
2314
|
+
{ id: "mu-kg", name: "Kilogram", abbreviation: "kg", isActive: true },
|
|
2315
|
+
{ id: "mu-g", name: "Gram", abbreviation: "g", isActive: true },
|
|
2316
|
+
{ id: "mu-l", name: "Liter", abbreviation: "L", isActive: true },
|
|
2317
|
+
{ id: "mu-ml", name: "Milliliter", abbreviation: "mL", isActive: true }
|
|
2318
|
+
]
|
|
2319
|
+
}
|
|
2320
|
+
];
|
|
2321
|
+
|
|
2322
|
+
// src/locales/en.ts
|
|
2323
|
+
var en = {
|
|
2324
|
+
"inventory.dashboard.activeItems": "Active items",
|
|
2325
|
+
"inventory.dashboard.belowMinimum": "Below minimum",
|
|
2326
|
+
"inventory.dashboard.entries": "Entries:",
|
|
2327
|
+
"inventory.dashboard.exits": "Exits:",
|
|
2328
|
+
"inventory.dashboard.last7Days": "Last 7 days",
|
|
2329
|
+
"inventory.dashboard.lowStock": "Low Stock",
|
|
2330
|
+
"inventory.dashboard.needRestocking": "Need restocking",
|
|
2331
|
+
"inventory.dashboard.outOfStock": "Out of Stock",
|
|
2332
|
+
"inventory.dashboard.quickActions": "Quick Actions",
|
|
2333
|
+
"inventory.dashboard.quickActionsDesc": "Use the sidebar to record stock entries, exits, or manage products.",
|
|
2334
|
+
"inventory.dashboard.recentActivity": "Recent Activity",
|
|
2335
|
+
"inventory.dashboard.stockValue": "Stock Value",
|
|
2336
|
+
"inventory.dashboard.totalProducts": "Total Products",
|
|
2337
|
+
"inventory.dashboard.totalValue": "Total inventory value",
|
|
2338
|
+
"inventory.history.noMovements": "No movements recorded",
|
|
2339
|
+
"inventory.history.searchPlaceholder": "Search movements...",
|
|
2340
|
+
"inventory.history.subtitle": "All stock movements",
|
|
2341
|
+
"inventory.history.title": "Stock History",
|
|
2342
|
+
"inventory.nav.dashboard": "Dashboard",
|
|
2343
|
+
"inventory.nav.entry": "Entry",
|
|
2344
|
+
"inventory.nav.exit": "Exit",
|
|
2345
|
+
"inventory.nav.history": "History",
|
|
2346
|
+
"inventory.nav.list": "List",
|
|
2347
|
+
"inventory.nav.new": "New",
|
|
2348
|
+
"inventory.nav.products": "Products",
|
|
2349
|
+
"inventory.nav.stock": "Stock",
|
|
2350
|
+
"inventory.page.newProduct": "New Product",
|
|
2351
|
+
"inventory.page.newProductDesc": "Add a product to your catalog",
|
|
2352
|
+
"inventory.page.settingsSubtitle": "Preferences, suppliers, categories, and units",
|
|
2353
|
+
"inventory.page.settingsTitle": "Inventory Settings",
|
|
2354
|
+
"inventory.page.stockEntry": "Stock Entry",
|
|
2355
|
+
"inventory.page.stockEntryDesc": "Record goods received",
|
|
2356
|
+
"inventory.page.stockExit": "Stock Exit",
|
|
2357
|
+
"inventory.page.stockExitDesc": "Record goods used or sold",
|
|
2358
|
+
"inventory.recipeDetail.active": "Active",
|
|
2359
|
+
"inventory.recipeDetail.backToList": "Back to list",
|
|
2360
|
+
"inventory.recipeDetail.inactive": "Inactive",
|
|
2361
|
+
"inventory.recipeDetail.ingredientCount": "Ingredients",
|
|
2362
|
+
"inventory.recipeDetail.ingredientsTitle": "Ingredients",
|
|
2363
|
+
"inventory.recipeDetail.instructions": "Instructions",
|
|
2364
|
+
"inventory.recipeDetail.noIngredients": "No ingredients defined",
|
|
2365
|
+
"inventory.recipeDetail.notFound": "Recipe not found",
|
|
2366
|
+
"inventory.recipeDetail.prepTime": "Prep time",
|
|
2367
|
+
"inventory.recipeDetail.produces": "Produces",
|
|
2368
|
+
"inventory.recipeDetail.recipes": "Recipes",
|
|
2369
|
+
"inventory.recipeDetail.yield": "Yield",
|
|
2370
|
+
"inventory.recipes.createFirst": "Create your first recipe",
|
|
2371
|
+
"inventory.recipes.ingredients": "Ingredients",
|
|
2372
|
+
"inventory.recipes.newRecipe": "New Recipe",
|
|
2373
|
+
"inventory.recipes.noRecipes": "No recipes yet",
|
|
2374
|
+
"inventory.recipes.produces": "Produces:",
|
|
2375
|
+
"inventory.recipes.productionFormulas": "{{count}} production formulas",
|
|
2376
|
+
"inventory.recipes.recipesDesc": "Recipes define how to produce products from ingredients",
|
|
2377
|
+
"inventory.recipes.title": "Recipes",
|
|
2378
|
+
"inventory.recipes.yield": "Yield",
|
|
2379
|
+
"inventory.stock.additionalDetails": "Additional details (optional)",
|
|
2380
|
+
"inventory.stock.adjustmentDesc": "Count correction",
|
|
2381
|
+
"inventory.stock.adjustmentLabel": "Adjustment",
|
|
2382
|
+
"inventory.stock.back": "Back",
|
|
2383
|
+
"inventory.stock.batchNumber": "Batch Number",
|
|
2384
|
+
"inventory.stock.currentStock": "Current Stock",
|
|
2385
|
+
"inventory.stock.destination": "Destination",
|
|
2386
|
+
"inventory.stock.document": "Document",
|
|
2387
|
+
"inventory.stock.documentNumber": "Document Number",
|
|
2388
|
+
"inventory.stock.entry": "Stock Entry",
|
|
2389
|
+
"inventory.stock.entryDesc": "Receiving goods",
|
|
2390
|
+
"inventory.stock.entryLabel": "Entry",
|
|
2391
|
+
"inventory.stock.exit": "Stock Exit",
|
|
2392
|
+
"inventory.stock.exitDesc": "Using or selling",
|
|
2393
|
+
"inventory.stock.exitLabel": "Exit",
|
|
2394
|
+
"inventory.stock.expirationDate": "Expiration Date",
|
|
2395
|
+
"inventory.stock.fromLocation": "From Location",
|
|
2396
|
+
"inventory.stock.locationLabel": "Location",
|
|
2397
|
+
"inventory.stock.lossDesc": "Waste or damage",
|
|
2398
|
+
"inventory.stock.lossLabel": "Loss",
|
|
2399
|
+
"inventory.stock.movement": "Stock Movement",
|
|
2400
|
+
"inventory.stock.movementDetails": "Movement Details",
|
|
2401
|
+
"inventory.stock.movementType": "Movement type",
|
|
2402
|
+
"inventory.stock.next": "Next",
|
|
2403
|
+
"inventory.stock.notes": "Notes",
|
|
2404
|
+
"inventory.stock.product": "Product",
|
|
2405
|
+
"inventory.stock.quantity": "Quantity *",
|
|
2406
|
+
"inventory.stock.quantityLabel": "Quantity",
|
|
2407
|
+
"inventory.stock.reason": "Reason *",
|
|
2408
|
+
"inventory.stock.reasonLabel": "Reason",
|
|
2409
|
+
"inventory.stock.recordMovement": "Record Movement",
|
|
2410
|
+
"inventory.stock.recorded": "Recorded",
|
|
2411
|
+
"inventory.stock.recording": "Recording...",
|
|
2412
|
+
"inventory.stock.searchProduct": "Search by name, SKU, or barcode...",
|
|
2413
|
+
"inventory.stock.stepOf": "Step {{step}} of 3",
|
|
2414
|
+
"inventory.stock.supplier": "Supplier",
|
|
2415
|
+
"inventory.stock.toLocation": "To Location *",
|
|
2416
|
+
"inventory.stock.total": "Total",
|
|
2417
|
+
"inventory.stock.totalCost": "Total Cost",
|
|
2418
|
+
"inventory.stock.transferDesc": "Between locations",
|
|
2419
|
+
"inventory.stock.transferLabel": "Transfer",
|
|
2420
|
+
"inventory.stock.unitCost": "Unit Cost",
|
|
2421
|
+
"inventory.stock.unitCostLabel": "Unit Cost",
|
|
2422
|
+
"inventory.onboarding.welcome": "Welcome to Inventory",
|
|
2423
|
+
"inventory.onboarding.description": "Track products, manage stock levels, and monitor movements across your business.",
|
|
2424
|
+
"inventory.onboarding.skip": "Skip setup",
|
|
2425
|
+
"inventory.onboarding.getStarted": "Get Started",
|
|
2426
|
+
"inventory.onboarding.units.title": "Measurement Units",
|
|
2427
|
+
"inventory.onboarding.units.description": "Default units (Unit, Box, Kg, L, etc.) are ready. Customize them anytime in Settings.",
|
|
2428
|
+
"inventory.onboarding.locations.title": "Stock Locations",
|
|
2429
|
+
"inventory.onboarding.locations.description": "A default storage location has been created. Add more for multi-location tracking.",
|
|
2430
|
+
"inventory.onboarding.start": "Start using Inventory",
|
|
2431
|
+
"inventory.quickActions.newProduct": "New Product",
|
|
2432
|
+
"inventory.quickActions.newProductDesc": "Add a product to your catalog",
|
|
2433
|
+
"inventory.quickActions.stockEntry": "Stock Entry",
|
|
2434
|
+
"inventory.quickActions.stockEntryDesc": "Record goods received",
|
|
2435
|
+
"inventory.quickActions.stockExit": "Stock Exit",
|
|
2436
|
+
"inventory.quickActions.stockExitDesc": "Record goods used or sold",
|
|
2437
|
+
"inventory.settingsPage.title": "Inventory Settings",
|
|
2438
|
+
"inventory.settingsPage.subtitle": "Manage units, categories, and stock locations",
|
|
2439
|
+
"inventory.settings.stockManagement": "Stock Management",
|
|
2440
|
+
"inventory.settings.stockManagementDesc": "How stock levels are tracked",
|
|
2441
|
+
"inventory.settings.lowStockAlerts": "Low stock alerts",
|
|
2442
|
+
"inventory.settings.lowStockAlertsDesc": "Show warnings when products fall below minimum quantity",
|
|
2443
|
+
"inventory.settings.requireReason": "Require reason for adjustments",
|
|
2444
|
+
"inventory.settings.requireReasonDesc": "Make reason mandatory for stock adjustments and losses",
|
|
2445
|
+
"inventory.settings.autoDeduct": "Auto-deduct on service",
|
|
2446
|
+
"inventory.settings.autoDeductDesc": "Automatically deduct products when a service is executed",
|
|
2447
|
+
"inventory.settings.products": "Products",
|
|
2448
|
+
"inventory.settings.productsDesc": "Product catalog behavior",
|
|
2449
|
+
"inventory.settings.requireSku": "Require SKU",
|
|
2450
|
+
"inventory.settings.requireSkuDesc": "Make SKU mandatory when creating products",
|
|
2451
|
+
"inventory.settings.allowNegative": "Allow negative stock",
|
|
2452
|
+
"inventory.settings.allowNegativeDesc": "Allow stock quantities to go below zero",
|
|
2453
|
+
"inventory.settings.notifications": "Notifications",
|
|
2454
|
+
"inventory.settings.notificationsDesc": "Alerts and reminders",
|
|
2455
|
+
"inventory.settings.lowStockEmail": "Low stock email alerts",
|
|
2456
|
+
"inventory.settings.lowStockEmailDesc": "Send email when products reach minimum quantity",
|
|
2457
|
+
"inventory.settings.expiryWarnings": "Expiry date warnings",
|
|
2458
|
+
"inventory.settings.expiryWarningsDesc": "Alert before products expire (batch tracking)",
|
|
2459
|
+
"inventory.productForm.newProduct": "New Product",
|
|
2460
|
+
"inventory.productForm.editProduct": "Edit Product",
|
|
2461
|
+
"inventory.productForm.updateDetails": "Update product details",
|
|
2462
|
+
"inventory.productForm.addToCatalog": "Add a product to your catalog",
|
|
2463
|
+
"inventory.productForm.cancel": "Cancel",
|
|
2464
|
+
"inventory.productForm.save": "Save",
|
|
2465
|
+
"inventory.productForm.saving": "Saving...",
|
|
2466
|
+
"inventory.productForm.generalInfo": "General Information",
|
|
2467
|
+
"inventory.productForm.name": "Name",
|
|
2468
|
+
"inventory.productForm.namePlaceholder": "Product name",
|
|
2469
|
+
"inventory.productForm.brand": "Brand",
|
|
2470
|
+
"inventory.productForm.brandPlaceholder": "Brand name",
|
|
2471
|
+
"inventory.productForm.sku": "SKU",
|
|
2472
|
+
"inventory.productForm.skuPlaceholder": "Internal code",
|
|
2473
|
+
"inventory.productForm.barcode": "Barcode",
|
|
2474
|
+
"inventory.productForm.barcodePlaceholder": "EAN / UPC",
|
|
2475
|
+
"inventory.productForm.description": "Description",
|
|
2476
|
+
"inventory.productForm.descriptionPlaceholder": "Additional information about the product",
|
|
2477
|
+
"inventory.productForm.classification": "Classification",
|
|
2478
|
+
"inventory.productForm.classificationDesc": "How this product is used in your business",
|
|
2479
|
+
"inventory.productForm.typeIngredient": "Raw material used in production or services",
|
|
2480
|
+
"inventory.productForm.typeSale": "Sold directly to customers",
|
|
2481
|
+
"inventory.productForm.typeIntermediate": "Produced internally from other items",
|
|
2482
|
+
"inventory.productForm.typeAsset": "Fixed asset for patrimony tracking",
|
|
2483
|
+
"inventory.productForm.pricing": "Pricing",
|
|
2484
|
+
"inventory.productForm.costPrice": "Cost Price",
|
|
2485
|
+
"inventory.productForm.salePrice": "Sale Price",
|
|
2486
|
+
"inventory.productForm.margin": "Margin",
|
|
2487
|
+
"inventory.productForm.stockLevels": "Stock Levels",
|
|
2488
|
+
"inventory.productForm.stockLevelsDesc": "Minimum and maximum thresholds for alerts",
|
|
2489
|
+
"inventory.productForm.minQuantity": "Minimum Quantity",
|
|
2490
|
+
"inventory.productForm.minQuantityHint": "Alert when stock falls below this",
|
|
2491
|
+
"inventory.productForm.maxQuantity": "Maximum Quantity",
|
|
2492
|
+
"inventory.productForm.maxQuantityHint": "Max capacity for this product",
|
|
2493
|
+
"inventory.productForm.optional": "Optional",
|
|
2494
|
+
"inventory.productList.title": "Products",
|
|
2495
|
+
"inventory.productList.subtitle": "{{count}} products",
|
|
2496
|
+
"inventory.productList.product": "Product",
|
|
2497
|
+
"inventory.productList.type": "Type",
|
|
2498
|
+
"inventory.productList.stock": "Stock",
|
|
2499
|
+
"inventory.productList.cost": "Cost",
|
|
2500
|
+
"inventory.productList.value": "Value",
|
|
2501
|
+
"inventory.productList.searchPlaceholder": "Search products...",
|
|
2502
|
+
"inventory.productList.newProduct": "New Product",
|
|
2503
|
+
"inventory.productList.empty": "No products yet",
|
|
2504
|
+
"inventory.productList.createFirst": "Create your first product",
|
|
2505
|
+
"inventory.stock.unknownProduct": "Unknown Product",
|
|
2506
|
+
"inventory.stock.reasonLossPlaceholder": "e.g. Expired, damaged",
|
|
2507
|
+
"inventory.stock.reasonAdjustPlaceholder": "e.g. Physical count correction",
|
|
2508
|
+
"inventory.stock.documentPlaceholder": "Invoice, receipt, PO...",
|
|
2509
|
+
"inventory.stock.batchPlaceholder": "e.g. LOT001",
|
|
2510
|
+
"inventory.stock.notesPlaceholder": "Additional notes...",
|
|
2511
|
+
"inventory.recipeDetail.loading": "Loading...",
|
|
2512
|
+
"inventory.recipeDetail.ingredient": "Ingredient",
|
|
2513
|
+
"inventory.recipeDetail.quantity": "Quantity",
|
|
2514
|
+
"inventory.recipeDetail.unit": "Unit",
|
|
2515
|
+
"inventory.recipeDetail.notes": "Notes",
|
|
2516
|
+
"inventory.recipeDetail.created": "Created",
|
|
2517
|
+
"inventory.recipeDetail.updated": "Updated",
|
|
2518
|
+
"inventory.recipeForm.newRecipe": "New Recipe",
|
|
2519
|
+
"inventory.recipeForm.subtitle": "Define a production formula",
|
|
2520
|
+
"inventory.recipeForm.cancel": "Cancel",
|
|
2521
|
+
"inventory.recipeForm.saveRecipe": "Save Recipe",
|
|
2522
|
+
"inventory.recipeForm.saving": "Saving...",
|
|
2523
|
+
"inventory.recipeForm.recipeDetails": "Recipe Details",
|
|
2524
|
+
"inventory.recipeForm.recipeName": "Recipe Name",
|
|
2525
|
+
"inventory.recipeForm.recipeNamePlaceholder": "e.g. Tomato Sauce",
|
|
2526
|
+
"inventory.recipeForm.description": "Description",
|
|
2527
|
+
"inventory.recipeForm.descriptionPlaceholder": "Brief description...",
|
|
2528
|
+
"inventory.recipeForm.produces": "Produces (Product)",
|
|
2529
|
+
"inventory.recipeForm.searchProduct": "Search product...",
|
|
2530
|
+
"inventory.recipeForm.createProduct": "Create product",
|
|
2531
|
+
"inventory.recipeForm.yieldQuantity": "Yield Quantity",
|
|
2532
|
+
"inventory.recipeForm.prepTime": "Prep Time (min)",
|
|
2533
|
+
"inventory.recipeForm.instructions": "Instructions",
|
|
2534
|
+
"inventory.recipeForm.instructionsPlaceholder": "Step-by-step preparation instructions...",
|
|
2535
|
+
"inventory.recipeForm.ingredients": "Ingredients",
|
|
2536
|
+
"inventory.recipeForm.addIngredient": "Add Ingredient",
|
|
2537
|
+
"inventory.recipeForm.noIngredients": "No ingredients added",
|
|
2538
|
+
"inventory.recipeForm.addFirstIngredient": "Add your first ingredient",
|
|
2539
|
+
"inventory.recipeForm.ingredient": "Ingredient",
|
|
2540
|
+
"inventory.recipeForm.quantity": "Quantity",
|
|
2541
|
+
"inventory.recipeForm.notes": "Notes",
|
|
2542
|
+
"inventory.recipeForm.searchIngredient": "Search ingredient...",
|
|
2543
|
+
"inventory.recipeForm.createIngredient": "Create ingredient",
|
|
2544
|
+
"inventory.recipeForm.notesPlaceholder": "e.g. diced, fresh",
|
|
2545
|
+
"inventory.recipeForm.ingredientsConfigured": "{{configured}} of {{total}} ingredients configured",
|
|
2546
|
+
"inventory.title": "Inventory",
|
|
2547
|
+
"inventory.stock.title": "Stock",
|
|
2548
|
+
"inventory.stock.minStock": "Min. Stock",
|
|
2549
|
+
"inventory.stock.adjust": "Adjust Stock",
|
|
2550
|
+
"inventory.stock.lowStock": "Low Stock",
|
|
2551
|
+
"inventory.products.title": "Products",
|
|
2552
|
+
"inventory.products.create": "New Product",
|
|
2553
|
+
"inventory.recipes.create": "New Recipe",
|
|
2554
|
+
"inventory.recipes.addIngredient": "Add Ingredient",
|
|
2555
|
+
"inventory.recipes.cost": "Cost",
|
|
2556
|
+
"inventory.recipes.loading": "Loading...",
|
|
2557
|
+
"inventory.suppliers.title": "Suppliers",
|
|
2558
|
+
"inventory.suppliers.create": "New Supplier",
|
|
2559
|
+
"inventory.movements.title": "Movements",
|
|
2560
|
+
"inventory.movements.in": "Stock In",
|
|
2561
|
+
"inventory.movements.out": "Stock Out",
|
|
2562
|
+
"inventory.settings.title": "Inventory Settings"
|
|
2563
|
+
};
|
|
2564
|
+
|
|
2565
|
+
// src/locales/pt-BR.ts
|
|
2566
|
+
var ptBR = {
|
|
2567
|
+
"inventory.dashboard.activeItems": "Itens ativos",
|
|
2568
|
+
"inventory.dashboard.belowMinimum": "Abaixo do m\xEDnimo",
|
|
2569
|
+
"inventory.dashboard.entries": "Entradas:",
|
|
2570
|
+
"inventory.dashboard.exits": "Sa\xEDdas:",
|
|
2571
|
+
"inventory.dashboard.last7Days": "\xDAltimos 7 dias",
|
|
2572
|
+
"inventory.dashboard.lowStock": "Estoque Baixo",
|
|
2573
|
+
"inventory.dashboard.needRestocking": "Precisam reposi\xE7\xE3o",
|
|
2574
|
+
"inventory.dashboard.outOfStock": "Sem Estoque",
|
|
2575
|
+
"inventory.dashboard.quickActions": "A\xE7\xF5es R\xE1pidas",
|
|
2576
|
+
"inventory.dashboard.quickActionsDesc": "Use a barra lateral para registrar entradas, sa\xEDdas ou gerenciar produtos.",
|
|
2577
|
+
"inventory.dashboard.recentActivity": "Atividade Recente",
|
|
2578
|
+
"inventory.dashboard.stockValue": "Valor do Estoque",
|
|
2579
|
+
"inventory.dashboard.totalProducts": "Total de Produtos",
|
|
2580
|
+
"inventory.dashboard.totalValue": "Valor total do estoque",
|
|
2581
|
+
"inventory.history.noMovements": "Nenhuma movimenta\xE7\xE3o registrada",
|
|
2582
|
+
"inventory.history.searchPlaceholder": "Buscar movimenta\xE7\xF5es...",
|
|
2583
|
+
"inventory.history.subtitle": "Todas as movimenta\xE7\xF5es de estoque",
|
|
2584
|
+
"inventory.history.title": "Hist\xF3rico de Estoque",
|
|
2585
|
+
"inventory.nav.dashboard": "Painel",
|
|
2586
|
+
"inventory.nav.entry": "Entrada",
|
|
2587
|
+
"inventory.nav.exit": "Sa\xEDda",
|
|
2588
|
+
"inventory.nav.history": "Hist\xF3rico",
|
|
2589
|
+
"inventory.nav.list": "Lista",
|
|
2590
|
+
"inventory.nav.new": "Novo",
|
|
2591
|
+
"inventory.nav.products": "Produtos",
|
|
2592
|
+
"inventory.nav.stock": "Estoque",
|
|
2593
|
+
"inventory.page.newProduct": "Novo Produto",
|
|
2594
|
+
"inventory.page.newProductDesc": "Adicionar um produto ao seu cat\xE1logo",
|
|
2595
|
+
"inventory.page.settingsSubtitle": "Prefer\xEAncias, fornecedores, categorias e unidades",
|
|
2596
|
+
"inventory.page.settingsTitle": "Configura\xE7\xF5es de Estoque",
|
|
2597
|
+
"inventory.page.stockEntry": "Entrada de Estoque",
|
|
2598
|
+
"inventory.page.stockEntryDesc": "Registrar mercadorias recebidas",
|
|
2599
|
+
"inventory.page.stockExit": "Sa\xEDda de Estoque",
|
|
2600
|
+
"inventory.page.stockExitDesc": "Registrar mercadorias usadas ou vendidas",
|
|
2601
|
+
"inventory.recipeDetail.active": "Ativa",
|
|
2602
|
+
"inventory.recipeDetail.backToList": "Voltar \xE0 lista",
|
|
2603
|
+
"inventory.recipeDetail.inactive": "Inativa",
|
|
2604
|
+
"inventory.recipeDetail.ingredientCount": "Ingredientes",
|
|
2605
|
+
"inventory.recipeDetail.ingredientsTitle": "Ingredientes",
|
|
2606
|
+
"inventory.recipeDetail.instructions": "Instru\xE7\xF5es",
|
|
2607
|
+
"inventory.recipeDetail.noIngredients": "Nenhum ingrediente definido",
|
|
2608
|
+
"inventory.recipeDetail.notFound": "Receita n\xE3o encontrada",
|
|
2609
|
+
"inventory.recipeDetail.prepTime": "Tempo de preparo",
|
|
2610
|
+
"inventory.recipeDetail.produces": "Produz",
|
|
2611
|
+
"inventory.recipeDetail.recipes": "Receitas",
|
|
2612
|
+
"inventory.recipeDetail.yield": "Rendimento",
|
|
2613
|
+
"inventory.recipes.createFirst": "Crie sua primeira receita",
|
|
2614
|
+
"inventory.recipes.ingredients": "Ingredientes",
|
|
2615
|
+
"inventory.recipes.newRecipe": "Nova Receita",
|
|
2616
|
+
"inventory.recipes.noRecipes": "Nenhuma receita ainda",
|
|
2617
|
+
"inventory.recipes.produces": "Produz:",
|
|
2618
|
+
"inventory.recipes.productionFormulas": "{{count}} f\xF3rmulas de produ\xE7\xE3o",
|
|
2619
|
+
"inventory.recipes.recipesDesc": "Receitas definem como produzir produtos a partir de ingredientes",
|
|
2620
|
+
"inventory.recipes.title": "Receitas",
|
|
2621
|
+
"inventory.recipes.yield": "Rendimento",
|
|
2622
|
+
"inventory.stock.additionalDetails": "Detalhes adicionais (opcional)",
|
|
2623
|
+
"inventory.stock.adjustmentDesc": "Corre\xE7\xE3o de contagem",
|
|
2624
|
+
"inventory.stock.adjustmentLabel": "Ajuste",
|
|
2625
|
+
"inventory.stock.back": "Voltar",
|
|
2626
|
+
"inventory.stock.batchNumber": "N\xFAmero do Lote",
|
|
2627
|
+
"inventory.stock.currentStock": "Estoque Atual",
|
|
2628
|
+
"inventory.stock.destination": "Destino",
|
|
2629
|
+
"inventory.stock.document": "Documento",
|
|
2630
|
+
"inventory.stock.documentNumber": "N\xFAmero do Documento",
|
|
2631
|
+
"inventory.stock.entry": "Entrada de Estoque",
|
|
2632
|
+
"inventory.stock.entryDesc": "Recebimento de mercadorias",
|
|
2633
|
+
"inventory.stock.entryLabel": "Entrada",
|
|
2634
|
+
"inventory.stock.exit": "Sa\xEDda de Estoque",
|
|
2635
|
+
"inventory.stock.exitDesc": "Uso ou venda",
|
|
2636
|
+
"inventory.stock.exitLabel": "Sa\xEDda",
|
|
2637
|
+
"inventory.stock.expirationDate": "Data de Validade",
|
|
2638
|
+
"inventory.stock.fromLocation": "Local de Origem",
|
|
2639
|
+
"inventory.stock.locationLabel": "Local",
|
|
2640
|
+
"inventory.stock.lossDesc": "Desperd\xEDcio ou dano",
|
|
2641
|
+
"inventory.stock.lossLabel": "Perda",
|
|
2642
|
+
"inventory.stock.movement": "Movimenta\xE7\xE3o de Estoque",
|
|
2643
|
+
"inventory.stock.movementDetails": "Detalhes da Movimenta\xE7\xE3o",
|
|
2644
|
+
"inventory.stock.movementType": "Tipo de movimenta\xE7\xE3o",
|
|
2645
|
+
"inventory.stock.next": "Pr\xF3ximo",
|
|
2646
|
+
"inventory.stock.notes": "Observa\xE7\xF5es",
|
|
2647
|
+
"inventory.stock.product": "Produto",
|
|
2648
|
+
"inventory.stock.quantity": "Quantidade *",
|
|
2649
|
+
"inventory.stock.quantityLabel": "Quantidade",
|
|
2650
|
+
"inventory.stock.reason": "Motivo *",
|
|
2651
|
+
"inventory.stock.reasonLabel": "Motivo",
|
|
2652
|
+
"inventory.stock.recordMovement": "Registrar Movimenta\xE7\xE3o",
|
|
2653
|
+
"inventory.stock.recorded": "Registrado",
|
|
2654
|
+
"inventory.stock.recording": "Registrando...",
|
|
2655
|
+
"inventory.stock.searchProduct": "Buscar por nome, SKU ou c\xF3digo de barras...",
|
|
2656
|
+
"inventory.stock.stepOf": "Etapa {{step}} de 3",
|
|
2657
|
+
"inventory.stock.supplier": "Fornecedor",
|
|
2658
|
+
"inventory.stock.toLocation": "Local de Destino *",
|
|
2659
|
+
"inventory.stock.total": "Total",
|
|
2660
|
+
"inventory.stock.totalCost": "Custo Total",
|
|
2661
|
+
"inventory.stock.transferDesc": "Entre locais",
|
|
2662
|
+
"inventory.stock.transferLabel": "Transfer\xEAncia",
|
|
2663
|
+
"inventory.stock.unitCost": "Custo Unit\xE1rio",
|
|
2664
|
+
"inventory.stock.unitCostLabel": "Custo Unit\xE1rio",
|
|
2665
|
+
"inventory.onboarding.welcome": "Bem-vindo ao Estoque",
|
|
2666
|
+
"inventory.onboarding.description": "Acompanhe produtos, gerencie n\xEDveis de estoque e monitore movimenta\xE7\xF5es do seu neg\xF3cio.",
|
|
2667
|
+
"inventory.onboarding.skip": "Pular configura\xE7\xE3o",
|
|
2668
|
+
"inventory.onboarding.getStarted": "Come\xE7ar",
|
|
2669
|
+
"inventory.onboarding.units.title": "Unidades de Medida",
|
|
2670
|
+
"inventory.onboarding.units.description": "Unidades padr\xE3o (Unidade, Caixa, Kg, L, etc.) est\xE3o prontas. Personalize-as a qualquer momento em Configura\xE7\xF5es.",
|
|
2671
|
+
"inventory.onboarding.locations.title": "Locais de Estoque",
|
|
2672
|
+
"inventory.onboarding.locations.description": "Um local de armazenamento padr\xE3o foi criado. Adicione mais para controle multi-local.",
|
|
2673
|
+
"inventory.onboarding.start": "Come\xE7ar a usar Estoque",
|
|
2674
|
+
"inventory.quickActions.newProduct": "Novo Produto",
|
|
2675
|
+
"inventory.quickActions.newProductDesc": "Adicionar um produto ao cat\xE1logo",
|
|
2676
|
+
"inventory.quickActions.stockEntry": "Entrada de Estoque",
|
|
2677
|
+
"inventory.quickActions.stockEntryDesc": "Registrar mercadorias recebidas",
|
|
2678
|
+
"inventory.quickActions.stockExit": "Sa\xEDda de Estoque",
|
|
2679
|
+
"inventory.quickActions.stockExitDesc": "Registrar mercadorias usadas ou vendidas",
|
|
2680
|
+
"inventory.settingsPage.title": "Configura\xE7\xF5es de Estoque",
|
|
2681
|
+
"inventory.settingsPage.subtitle": "Gerenciar unidades, categorias e locais de estoque",
|
|
2682
|
+
"inventory.settings.stockManagement": "Gest\xE3o de Estoque",
|
|
2683
|
+
"inventory.settings.stockManagementDesc": "Como os n\xEDveis de estoque s\xE3o rastreados",
|
|
2684
|
+
"inventory.settings.lowStockAlerts": "Alertas de estoque baixo",
|
|
2685
|
+
"inventory.settings.lowStockAlertsDesc": "Mostrar avisos quando produtos ficarem abaixo da quantidade m\xEDnima",
|
|
2686
|
+
"inventory.settings.requireReason": "Exigir motivo para ajustes",
|
|
2687
|
+
"inventory.settings.requireReasonDesc": "Tornar obrigat\xF3rio o motivo para ajustes e perdas de estoque",
|
|
2688
|
+
"inventory.settings.autoDeduct": "Auto-deduzir no servi\xE7o",
|
|
2689
|
+
"inventory.settings.autoDeductDesc": "Deduzir automaticamente produtos quando um servi\xE7o \xE9 executado",
|
|
2690
|
+
"inventory.settings.products": "Produtos",
|
|
2691
|
+
"inventory.settings.productsDesc": "Comportamento do cat\xE1logo de produtos",
|
|
2692
|
+
"inventory.settings.requireSku": "Exigir SKU",
|
|
2693
|
+
"inventory.settings.requireSkuDesc": "Tornar SKU obrigat\xF3rio ao criar produtos",
|
|
2694
|
+
"inventory.settings.allowNegative": "Permitir estoque negativo",
|
|
2695
|
+
"inventory.settings.allowNegativeDesc": "Permitir que quantidades de estoque fiquem abaixo de zero",
|
|
2696
|
+
"inventory.settings.notifications": "Notifica\xE7\xF5es",
|
|
2697
|
+
"inventory.settings.notificationsDesc": "Alertas e lembretes",
|
|
2698
|
+
"inventory.settings.lowStockEmail": "Alertas de estoque baixo por e-mail",
|
|
2699
|
+
"inventory.settings.lowStockEmailDesc": "Enviar e-mail quando produtos atingirem a quantidade m\xEDnima",
|
|
2700
|
+
"inventory.settings.expiryWarnings": "Avisos de validade",
|
|
2701
|
+
"inventory.settings.expiryWarningsDesc": "Alertar antes de produtos vencerem (rastreamento de lotes)",
|
|
2702
|
+
"inventory.productForm.newProduct": "Novo Produto",
|
|
2703
|
+
"inventory.productForm.editProduct": "Editar Produto",
|
|
2704
|
+
"inventory.productForm.updateDetails": "Atualizar detalhes do produto",
|
|
2705
|
+
"inventory.productForm.addToCatalog": "Adicionar um produto ao seu cat\xE1logo",
|
|
2706
|
+
"inventory.productForm.cancel": "Cancelar",
|
|
2707
|
+
"inventory.productForm.save": "Salvar",
|
|
2708
|
+
"inventory.productForm.saving": "Salvando...",
|
|
2709
|
+
"inventory.productForm.generalInfo": "Informa\xE7\xF5es Gerais",
|
|
2710
|
+
"inventory.productForm.name": "Nome",
|
|
2711
|
+
"inventory.productForm.namePlaceholder": "Nome do produto",
|
|
2712
|
+
"inventory.productForm.brand": "Marca",
|
|
2713
|
+
"inventory.productForm.brandPlaceholder": "Nome da marca",
|
|
2714
|
+
"inventory.productForm.sku": "SKU",
|
|
2715
|
+
"inventory.productForm.skuPlaceholder": "C\xF3digo interno",
|
|
2716
|
+
"inventory.productForm.barcode": "C\xF3digo de Barras",
|
|
2717
|
+
"inventory.productForm.barcodePlaceholder": "EAN / UPC",
|
|
2718
|
+
"inventory.productForm.description": "Descri\xE7\xE3o",
|
|
2719
|
+
"inventory.productForm.descriptionPlaceholder": "Informa\xE7\xF5es adicionais sobre o produto",
|
|
2720
|
+
"inventory.productForm.classification": "Classifica\xE7\xE3o",
|
|
2721
|
+
"inventory.productForm.classificationDesc": "Como este produto \xE9 usado no seu neg\xF3cio",
|
|
2722
|
+
"inventory.productForm.typeIngredient": "Mat\xE9ria-prima usada na produ\xE7\xE3o ou servi\xE7os",
|
|
2723
|
+
"inventory.productForm.typeSale": "Vendido diretamente aos clientes",
|
|
2724
|
+
"inventory.productForm.typeIntermediate": "Produzido internamente a partir de outros itens",
|
|
2725
|
+
"inventory.productForm.typeAsset": "Ativo fixo para controle patrimonial",
|
|
2726
|
+
"inventory.productForm.pricing": "Pre\xE7os",
|
|
2727
|
+
"inventory.productForm.costPrice": "Pre\xE7o de Custo",
|
|
2728
|
+
"inventory.productForm.salePrice": "Pre\xE7o de Venda",
|
|
2729
|
+
"inventory.productForm.margin": "Margem",
|
|
2730
|
+
"inventory.productForm.stockLevels": "N\xEDveis de Estoque",
|
|
2731
|
+
"inventory.productForm.stockLevelsDesc": "Limites m\xEDnimo e m\xE1ximo para alertas",
|
|
2732
|
+
"inventory.productForm.minQuantity": "Quantidade M\xEDnima",
|
|
2733
|
+
"inventory.productForm.minQuantityHint": "Alertar quando o estoque ficar abaixo deste valor",
|
|
2734
|
+
"inventory.productForm.maxQuantity": "Quantidade M\xE1xima",
|
|
2735
|
+
"inventory.productForm.maxQuantityHint": "Capacidade m\xE1xima para este produto",
|
|
2736
|
+
"inventory.productForm.optional": "Opcional",
|
|
2737
|
+
"inventory.productList.title": "Produtos",
|
|
2738
|
+
"inventory.productList.subtitle": "{{count}} produtos",
|
|
2739
|
+
"inventory.productList.product": "Produto",
|
|
2740
|
+
"inventory.productList.type": "Tipo",
|
|
2741
|
+
"inventory.productList.stock": "Estoque",
|
|
2742
|
+
"inventory.productList.cost": "Custo",
|
|
2743
|
+
"inventory.productList.value": "Valor",
|
|
2744
|
+
"inventory.productList.searchPlaceholder": "Buscar produtos...",
|
|
2745
|
+
"inventory.productList.newProduct": "Novo Produto",
|
|
2746
|
+
"inventory.productList.empty": "Nenhum produto ainda",
|
|
2747
|
+
"inventory.productList.createFirst": "Crie seu primeiro produto",
|
|
2748
|
+
"inventory.stock.unknownProduct": "Produto Desconhecido",
|
|
2749
|
+
"inventory.stock.reasonLossPlaceholder": "ex. Vencido, danificado",
|
|
2750
|
+
"inventory.stock.reasonAdjustPlaceholder": "ex. Corre\xE7\xE3o de contagem f\xEDsica",
|
|
2751
|
+
"inventory.stock.documentPlaceholder": "Nota fiscal, recibo, PO...",
|
|
2752
|
+
"inventory.stock.batchPlaceholder": "ex. LOT001",
|
|
2753
|
+
"inventory.stock.notesPlaceholder": "Observa\xE7\xF5es adicionais...",
|
|
2754
|
+
"inventory.recipeDetail.loading": "Carregando...",
|
|
2755
|
+
"inventory.recipeDetail.ingredient": "Ingrediente",
|
|
2756
|
+
"inventory.recipeDetail.quantity": "Quantidade",
|
|
2757
|
+
"inventory.recipeDetail.unit": "Unidade",
|
|
2758
|
+
"inventory.recipeDetail.notes": "Observa\xE7\xF5es",
|
|
2759
|
+
"inventory.recipeDetail.created": "Criado",
|
|
2760
|
+
"inventory.recipeDetail.updated": "Atualizado",
|
|
2761
|
+
"inventory.recipeForm.newRecipe": "Nova Receita",
|
|
2762
|
+
"inventory.recipeForm.subtitle": "Definir uma f\xF3rmula de produ\xE7\xE3o",
|
|
2763
|
+
"inventory.recipeForm.cancel": "Cancelar",
|
|
2764
|
+
"inventory.recipeForm.saveRecipe": "Salvar Receita",
|
|
2765
|
+
"inventory.recipeForm.saving": "Salvando...",
|
|
2766
|
+
"inventory.recipeForm.recipeDetails": "Detalhes da Receita",
|
|
2767
|
+
"inventory.recipeForm.recipeName": "Nome da Receita",
|
|
2768
|
+
"inventory.recipeForm.recipeNamePlaceholder": "ex. Molho de Tomate",
|
|
2769
|
+
"inventory.recipeForm.description": "Descri\xE7\xE3o",
|
|
2770
|
+
"inventory.recipeForm.descriptionPlaceholder": "Breve descri\xE7\xE3o...",
|
|
2771
|
+
"inventory.recipeForm.produces": "Produz (Produto)",
|
|
2772
|
+
"inventory.recipeForm.searchProduct": "Buscar produto...",
|
|
2773
|
+
"inventory.recipeForm.createProduct": "Criar produto",
|
|
2774
|
+
"inventory.recipeForm.yieldQuantity": "Quantidade de Rendimento",
|
|
2775
|
+
"inventory.recipeForm.prepTime": "Tempo de Preparo (min)",
|
|
2776
|
+
"inventory.recipeForm.instructions": "Instru\xE7\xF5es",
|
|
2777
|
+
"inventory.recipeForm.instructionsPlaceholder": "Instru\xE7\xF5es de preparo passo a passo...",
|
|
2778
|
+
"inventory.recipeForm.ingredients": "Ingredientes",
|
|
2779
|
+
"inventory.recipeForm.addIngredient": "Adicionar Ingrediente",
|
|
2780
|
+
"inventory.recipeForm.noIngredients": "Nenhum ingrediente adicionado",
|
|
2781
|
+
"inventory.recipeForm.addFirstIngredient": "Adicione seu primeiro ingrediente",
|
|
2782
|
+
"inventory.recipeForm.ingredient": "Ingrediente",
|
|
2783
|
+
"inventory.recipeForm.quantity": "Quantidade",
|
|
2784
|
+
"inventory.recipeForm.notes": "Observa\xE7\xF5es",
|
|
2785
|
+
"inventory.recipeForm.searchIngredient": "Buscar ingrediente...",
|
|
2786
|
+
"inventory.recipeForm.createIngredient": "Criar ingrediente",
|
|
2787
|
+
"inventory.recipeForm.notesPlaceholder": "ex. picado, fresco",
|
|
2788
|
+
"inventory.recipeForm.ingredientsConfigured": "{{configured}} de {{total}} ingredientes configurados",
|
|
2789
|
+
"inventory.title": "Estoque",
|
|
2790
|
+
"inventory.stock.title": "Estoque",
|
|
2791
|
+
"inventory.stock.minStock": "Estoque M\xEDn.",
|
|
2792
|
+
"inventory.stock.adjust": "Ajustar Estoque",
|
|
2793
|
+
"inventory.stock.lowStock": "Estoque Baixo",
|
|
2794
|
+
"inventory.products.title": "Produtos",
|
|
2795
|
+
"inventory.products.create": "Novo Produto",
|
|
2796
|
+
"inventory.recipes.create": "Nova Receita",
|
|
2797
|
+
"inventory.recipes.addIngredient": "Adicionar Ingrediente",
|
|
2798
|
+
"inventory.recipes.cost": "Custo",
|
|
2799
|
+
"inventory.recipes.loading": "Carregando...",
|
|
2800
|
+
"inventory.suppliers.title": "Fornecedores",
|
|
2801
|
+
"inventory.suppliers.create": "Novo Fornecedor",
|
|
2802
|
+
"inventory.movements.title": "Movimenta\xE7\xF5es",
|
|
2803
|
+
"inventory.movements.in": "Entrada",
|
|
2804
|
+
"inventory.movements.out": "Sa\xEDda",
|
|
2805
|
+
"inventory.settings.title": "Configura\xE7\xF5es de Estoque"
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
// src/locales/index.ts
|
|
2809
|
+
var inventoryLocales = {
|
|
2810
|
+
en,
|
|
2811
|
+
"pt-BR": ptBR
|
|
2812
|
+
};
|
|
2813
|
+
|
|
2814
|
+
// src/index.ts
|
|
2815
|
+
var DEFAULT_LABELS = {
|
|
2816
|
+
pageTitle: "Inventory",
|
|
2817
|
+
pageSubtitle: "Product catalog and stock management",
|
|
2818
|
+
dashboard: "Dashboard",
|
|
2819
|
+
products: "Products",
|
|
2820
|
+
productsNew: "New",
|
|
2821
|
+
productsList: "List",
|
|
2822
|
+
stock: "Stock",
|
|
2823
|
+
stockEntry: "Entry",
|
|
2824
|
+
stockExit: "Exit",
|
|
2825
|
+
stockHistory: "History",
|
|
2826
|
+
recipes: "Recipes",
|
|
2827
|
+
recipesNew: "New",
|
|
2828
|
+
recipesList: "List"
|
|
2829
|
+
};
|
|
2830
|
+
var DEFAULT_CURRENCY = { code: "BRL", locale: "pt-BR", symbol: "R$" };
|
|
2831
|
+
var DEFAULT_PRODUCT_TYPES = [
|
|
2832
|
+
{ value: "ingredient", label: "Ingredient" },
|
|
2833
|
+
{ value: "sale", label: "For Sale" },
|
|
2834
|
+
{ value: "intermediate", label: "Intermediate" },
|
|
2835
|
+
{ value: "asset", label: "Asset" }
|
|
2836
|
+
];
|
|
2837
|
+
function resolveConfig(options) {
|
|
2838
|
+
return {
|
|
2839
|
+
modules: {
|
|
2840
|
+
recipes: options?.modules?.recipes !== false,
|
|
2841
|
+
stockLocations: options?.modules?.stockLocations !== false,
|
|
2842
|
+
batchTracking: options?.modules?.batchTracking ?? false
|
|
2843
|
+
},
|
|
2844
|
+
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
2845
|
+
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
2846
|
+
productTypes: options?.productTypes ?? DEFAULT_PRODUCT_TYPES,
|
|
2847
|
+
locations: options?.locations ?? []
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
function createInventoryPlugin(options) {
|
|
2851
|
+
const config = resolveConfig(options);
|
|
2852
|
+
core.registerTranslations(inventoryLocales);
|
|
2853
|
+
const provider = options?.dataProvider ?? core.createSafeDataProvider(
|
|
2854
|
+
() => createSupabaseInventoryProvider(),
|
|
2855
|
+
() => createMockInventoryProvider()
|
|
2856
|
+
);
|
|
2857
|
+
const store = createInventoryStore(provider);
|
|
2858
|
+
const dashboardWidgets = createInventoryDashboardWidgets({ config, provider, store });
|
|
2859
|
+
const PageComponent = () => React11__default.default.createElement(InventoryPage, { config, provider, store, registries: inventoryRegistries });
|
|
2860
|
+
return {
|
|
2861
|
+
id: "inventory",
|
|
2862
|
+
name: config.labels.pageTitle,
|
|
2863
|
+
icon: "Package",
|
|
2864
|
+
version: "1.0.0",
|
|
2865
|
+
scope: options?.scope ?? "universal",
|
|
2866
|
+
verticalId: options?.verticalId,
|
|
2867
|
+
defaultEnabled: true,
|
|
2868
|
+
dependencies: [],
|
|
2869
|
+
declaredFeatures: [
|
|
2870
|
+
{ id: "inventory", label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
2871
|
+
...config.modules.recipes ? [{ id: "inventory.recipes", label: config.labels.recipes ?? "Recipes", group: config.labels.pageTitle }] : []
|
|
2872
|
+
],
|
|
2873
|
+
navigation: [
|
|
2874
|
+
{
|
|
2875
|
+
section: options?.navSection ?? "main",
|
|
2876
|
+
position: options?.navPosition ?? 4,
|
|
2877
|
+
label: config.labels.pageTitle,
|
|
2878
|
+
route: "/inventory",
|
|
2879
|
+
icon: "Package",
|
|
2880
|
+
permission: { feature: "inventory", action: "read" }
|
|
2881
|
+
}
|
|
2882
|
+
],
|
|
2883
|
+
routes: [
|
|
2884
|
+
{
|
|
2885
|
+
path: "/inventory",
|
|
2886
|
+
component: PageComponent,
|
|
2887
|
+
permission: { feature: "inventory", action: "read" }
|
|
2888
|
+
}
|
|
2889
|
+
],
|
|
2890
|
+
widgets: [],
|
|
2891
|
+
dashboardWidgets,
|
|
2892
|
+
aiTools: [
|
|
2893
|
+
{
|
|
2894
|
+
id: "inventory.low-stock",
|
|
2895
|
+
name: "getLowStock",
|
|
2896
|
+
description: "Lists products with stock below minimum threshold.",
|
|
2897
|
+
icon: "AlertTriangle",
|
|
2898
|
+
mode: "read",
|
|
2899
|
+
category: "Inventory",
|
|
2900
|
+
parameters: {
|
|
2901
|
+
type: "object",
|
|
2902
|
+
properties: {
|
|
2903
|
+
threshold: { type: "number", description: "Custom stock threshold" }
|
|
2904
|
+
}
|
|
2905
|
+
},
|
|
2906
|
+
suggestions: [
|
|
2907
|
+
{ label: "Which products are running low?" },
|
|
2908
|
+
{ label: "What ingredients need restocking?", verticalId: "food" },
|
|
2909
|
+
{ label: "Show me a stock summary" },
|
|
2910
|
+
{ label: "What are my most used products?" }
|
|
2911
|
+
],
|
|
2912
|
+
permission: { feature: "inventory", action: "read" }
|
|
2913
|
+
}
|
|
2914
|
+
],
|
|
2915
|
+
registries: inventoryRegistries,
|
|
2916
|
+
settings: [
|
|
2917
|
+
{
|
|
2918
|
+
id: "inventory",
|
|
2919
|
+
label: "Inventory",
|
|
2920
|
+
icon: "Package",
|
|
2921
|
+
component: (() => {
|
|
2922
|
+
const Tab = () => React11__default.default.createElement(InventoryGeneralSettings);
|
|
2923
|
+
Tab.displayName = "InventorySettingsTab";
|
|
2924
|
+
return Tab;
|
|
2925
|
+
})(),
|
|
2926
|
+
order: 11,
|
|
2927
|
+
permission: { feature: "inventory", action: "read" }
|
|
2928
|
+
}
|
|
2929
|
+
],
|
|
2930
|
+
locales: inventoryLocales
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
exports.createInventoryPlugin = createInventoryPlugin;
|
|
2935
|
+
//# sourceMappingURL=index.cjs.map
|
|
2936
|
+
//# sourceMappingURL=index.cjs.map
|