@fayz-ai/plugin-inventory 0.1.7 → 0.8.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/README.md +2 -0
- package/dist/InventoryContext.d.ts +2 -0
- package/dist/InventoryContext.d.ts.map +1 -1
- package/dist/InventoryPage.d.ts.map +1 -1
- package/dist/components/InventoryGeneralSettings.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +10 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/index.cjs +111 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -51
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +9 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/views/ProductCrudForm.d.ts.map +1 -1
- package/dist/views/ProductListView.d.ts.map +1 -1
- package/dist/views/RecipeFormView.d.ts.map +1 -1
- package/dist/views/RecipesView.d.ts.map +1 -1
- package/package.json +10 -4
- package/src/InventoryContext.tsx +2 -0
- package/src/InventoryPage.tsx +13 -7
- package/src/README.md +1 -1
- package/src/components/InventoryGeneralSettings.tsx +21 -8
- package/src/data/supabase.ts +24 -23
- package/src/data/tables.ts +10 -0
- package/src/index.ts +43 -0
- package/src/migrations/000_plg_rename.sql +27 -0
- package/src/migrations/001_inventory_base.sql +27 -27
- package/src/migrations/002_recipes.sql +13 -13
- package/src/migrations/003_measurement_units.sql +4 -4
- package/src/migrations/index.ts +161 -0
- package/src/registries.ts +5 -5
- package/src/views/ProductCrudForm.tsx +6 -1
- package/src/views/ProductListView.tsx +1 -0
- package/src/views/RecipeFormView.tsx +6 -0
- package/src/views/RecipesView.tsx +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,14 @@ export interface InventoryPluginOptions {
|
|
|
20
20
|
recipes?: boolean;
|
|
21
21
|
stockLocations?: boolean;
|
|
22
22
|
batchTracking?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The product catalogue screens (list + form). Defaults on. Turn OFF when the
|
|
25
|
+
* host already owns product registration elsewhere — an e-commerce app
|
|
26
|
+
* manages products in the shop plugin, and two competing product CRUDs is
|
|
27
|
+
* how the same product ends up entered twice, differently.
|
|
28
|
+
* Stock entry/exit/history stay available either way.
|
|
29
|
+
*/
|
|
30
|
+
products?: boolean;
|
|
23
31
|
};
|
|
24
32
|
labels?: Partial<InventoryPluginLabels>;
|
|
25
33
|
productTypes?: Array<{
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAI5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAazD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAI5E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAazD,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,cAAc,CAAC,EAAE,OAAO,CAAA;QACxB,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,CAAA;IACD,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAA;IACvC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;IACjC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,qBAAqB,CAAA;IACpC,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CAChE;AAsDD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,cAAc,CAuHtF;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React2, { useState, useMemo, useEffect } from 'react';
|
|
2
2
|
import { toast, defineKpiWidget, defineCustomWidget, PageTransition, ModulePage, Button, SubpageHeader, SearchSelect, cn, DatePicker, ListView, useSaveBar, Breadcrumb, DataTable, DashboardCanvas, KpiCard, Card, CardHeader, CardTitle, CardContent } from '@fayz-ai/ui';
|
|
3
|
-
import { createPluginContext, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, SettingsGroup, ToggleRow, CrudListView, CrudFormPage, formatCurrency } from '@fayz-ai/saas';
|
|
3
|
+
import { createPluginContext, dedup, useModuleNavigation, createViewRouter, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, CrudListView, useLimitGuard, CrudFormPage, formatCurrency, PermissionGate, invalidateLimit } from '@fayz-ai/saas';
|
|
4
4
|
import { registerTranslations, createSafeDataProvider, useTranslation, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
|
|
5
5
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
import { Package, Ruler, MapPin, Check, ChevronRight, ArrowDownRight, ArrowUpRight, RefreshCw, ArrowRightLeft, Trash2, Plus, BookOpen, Layers, Clock } from 'lucide-react';
|
|
@@ -167,7 +167,8 @@ function ProductListView({ onNew, onEdit }) {
|
|
|
167
167
|
onFacetChange: (_field, value) => setTypeFilter(value),
|
|
168
168
|
onNew,
|
|
169
169
|
addLabel: t2("inventory.productList.newProduct"),
|
|
170
|
-
onRowClick: (row) => onEdit?.(row.id)
|
|
170
|
+
onRowClick: (row) => onEdit?.(row.id),
|
|
171
|
+
feature: "inventory"
|
|
171
172
|
}
|
|
172
173
|
);
|
|
173
174
|
}
|
|
@@ -176,6 +177,7 @@ function ProductCrudForm({ editId, onSaved }) {
|
|
|
176
177
|
const { productTypes, currency } = useInventoryConfig();
|
|
177
178
|
const provider = useInventoryProvider();
|
|
178
179
|
const createProduct = useInventoryStore((s) => s.createProduct);
|
|
180
|
+
const guardProducts = useLimitGuard("products");
|
|
179
181
|
const isEdit = !!editId;
|
|
180
182
|
const [initialData, setInitialData] = React2.useState(isEdit ? null : {});
|
|
181
183
|
const [headerName, setHeaderName] = React2.useState("");
|
|
@@ -227,7 +229,9 @@ function ProductCrudForm({ editId, onSaved }) {
|
|
|
227
229
|
if (isEdit && editId) {
|
|
228
230
|
await provider.updateProduct(editId, payload);
|
|
229
231
|
} else {
|
|
232
|
+
if (await guardProducts() === "blocked") return;
|
|
230
233
|
await createProduct(payload);
|
|
234
|
+
invalidateLimit("products");
|
|
231
235
|
}
|
|
232
236
|
onSaved?.();
|
|
233
237
|
}
|
|
@@ -803,11 +807,11 @@ function RecipesView({ onNew, onView }) {
|
|
|
803
807
|
{
|
|
804
808
|
title: t2("inventory.recipes.title"),
|
|
805
809
|
subtitle: t2("inventory.recipes.productionFormulas", { count: String(recipes.length) }),
|
|
806
|
-
actions: onNew && /* @__PURE__ */ 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: [
|
|
810
|
+
actions: onNew && /* @__PURE__ */ jsx(PermissionGate, { feature: "inventory", action: "create", children: /* @__PURE__ */ 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: [
|
|
807
811
|
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
|
|
808
812
|
" ",
|
|
809
813
|
t2("inventory.recipes.newRecipe")
|
|
810
|
-
] })
|
|
814
|
+
] }) })
|
|
811
815
|
}
|
|
812
816
|
),
|
|
813
817
|
recipesLoading ? /* @__PURE__ */ jsx(RecipeSkeleton, {}) : recipes.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
@@ -866,6 +870,7 @@ function RecipeFormView({ onSaved }) {
|
|
|
866
870
|
const t2 = useTranslation();
|
|
867
871
|
const provider = useInventoryProvider();
|
|
868
872
|
const createRecipe = useInventoryStore((s) => s.createRecipe);
|
|
873
|
+
const guardRecipes = useLimitGuard("recipes");
|
|
869
874
|
const [name, setName] = useState("");
|
|
870
875
|
const [description, setDescription] = useState("");
|
|
871
876
|
const [productId, setProductId] = useState("");
|
|
@@ -889,6 +894,7 @@ function RecipeFormView({ onSaved }) {
|
|
|
889
894
|
toast.error(t2("common.formIncomplete"));
|
|
890
895
|
return;
|
|
891
896
|
}
|
|
897
|
+
if (await guardRecipes() === "blocked") return;
|
|
892
898
|
setSaving(true);
|
|
893
899
|
try {
|
|
894
900
|
const recipe = await createRecipe({
|
|
@@ -906,6 +912,7 @@ function RecipeFormView({ onSaved }) {
|
|
|
906
912
|
notes: i.notes || void 0
|
|
907
913
|
}))
|
|
908
914
|
});
|
|
915
|
+
invalidateLimit("recipes");
|
|
909
916
|
onSaved?.(recipe.id);
|
|
910
917
|
} finally {
|
|
911
918
|
setSaving(false);
|
|
@@ -1216,28 +1223,31 @@ function RecipeDetailView({ recipeId, onBack }) {
|
|
|
1216
1223
|
] })
|
|
1217
1224
|
] });
|
|
1218
1225
|
}
|
|
1226
|
+
var INVENTORY_DEFAULTS = {
|
|
1227
|
+
lowStockAlerts: true,
|
|
1228
|
+
requireReason: true,
|
|
1229
|
+
autoDeduct: false,
|
|
1230
|
+
requireSku: false,
|
|
1231
|
+
allowNegative: false,
|
|
1232
|
+
lowStockEmail: false,
|
|
1233
|
+
expiryWarnings: true
|
|
1234
|
+
};
|
|
1219
1235
|
function InventoryGeneralSettings() {
|
|
1220
1236
|
const t2 = useTranslation();
|
|
1237
|
+
const s = useTenantPluginSettings("inventory", INVENTORY_DEFAULTS);
|
|
1221
1238
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1222
1239
|
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
|
|
1223
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked:
|
|
1224
|
-
|
|
1225
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.
|
|
1226
|
-
} }),
|
|
1227
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: false, onChange: () => {
|
|
1228
|
-
} })
|
|
1240
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: s.get("lowStockAlerts"), onChange: (v) => s.set("lowStockAlerts", v) }),
|
|
1241
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: s.get("requireReason"), onChange: (v) => s.set("requireReason", v) }),
|
|
1242
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: s.get("autoDeduct"), onChange: (v) => s.set("autoDeduct", v) })
|
|
1229
1243
|
] }),
|
|
1230
1244
|
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.products"), description: t2("inventory.settings.productsDesc"), children: [
|
|
1231
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked:
|
|
1232
|
-
|
|
1233
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: false, onChange: () => {
|
|
1234
|
-
} })
|
|
1245
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: s.get("requireSku"), onChange: (v) => s.set("requireSku", v) }),
|
|
1246
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: s.get("allowNegative"), onChange: (v) => s.set("allowNegative", v) })
|
|
1235
1247
|
] }),
|
|
1236
1248
|
/* @__PURE__ */ jsxs(SettingsGroup, { title: t2("inventory.settings.notifications"), description: t2("inventory.settings.notificationsDesc"), children: [
|
|
1237
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked:
|
|
1238
|
-
|
|
1239
|
-
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: true, onChange: () => {
|
|
1240
|
-
} })
|
|
1249
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: s.get("lowStockEmail"), onChange: (v) => s.set("lowStockEmail", v) }),
|
|
1250
|
+
/* @__PURE__ */ jsx(ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: s.get("expiryWarnings"), onChange: (v) => s.set("expiryWarnings", v) })
|
|
1241
1251
|
] })
|
|
1242
1252
|
] });
|
|
1243
1253
|
}
|
|
@@ -1279,7 +1289,7 @@ function InventoryOnboarding({ onComplete }) {
|
|
|
1279
1289
|
function buildNav(config, view, navigate, t2) {
|
|
1280
1290
|
const items = [
|
|
1281
1291
|
{ id: "dashboard", label: t2("inventory.nav.dashboard"), icon: "BarChart3", active: view === "dashboard", onClick: () => navigate("dashboard") },
|
|
1282
|
-
{
|
|
1292
|
+
...config.modules.products ? [{
|
|
1283
1293
|
id: "products",
|
|
1284
1294
|
label: t2("inventory.nav.products"),
|
|
1285
1295
|
icon: "Package",
|
|
@@ -1288,7 +1298,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
1288
1298
|
{ id: "products-new", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
|
|
1289
1299
|
{ id: "products-list", label: t2("inventory.nav.list"), active: view === "products-list", onClick: () => navigate("products-list") }
|
|
1290
1300
|
]
|
|
1291
|
-
},
|
|
1301
|
+
}] : [],
|
|
1292
1302
|
{
|
|
1293
1303
|
id: "stock",
|
|
1294
1304
|
label: t2("inventory.nav.stock"),
|
|
@@ -1339,13 +1349,15 @@ function InventoryPage({ config, provider, store, registries }) {
|
|
|
1339
1349
|
const nav = buildNav(config, view, navigate, t2);
|
|
1340
1350
|
const quickActions = useMemo(() => {
|
|
1341
1351
|
const actions = [
|
|
1342
|
-
|
|
1352
|
+
// Dropped with the catalogue module: offering "new product" while the
|
|
1353
|
+
// screen is gone would dead-end the user.
|
|
1354
|
+
...config.modules.products ? [{
|
|
1343
1355
|
id: "new-product",
|
|
1344
1356
|
label: t2("inventory.quickActions.newProduct"),
|
|
1345
1357
|
icon: "Package",
|
|
1346
1358
|
description: t2("inventory.quickActions.newProductDesc"),
|
|
1347
1359
|
action: () => navigate("products-new")
|
|
1348
|
-
},
|
|
1360
|
+
}] : [],
|
|
1349
1361
|
{
|
|
1350
1362
|
id: "stock-entry",
|
|
1351
1363
|
label: t2("inventory.quickActions.stockEntry"),
|
|
@@ -1382,9 +1394,13 @@ function InventoryPage({ config, provider, store, registries }) {
|
|
|
1382
1394
|
] }) }) });
|
|
1383
1395
|
}
|
|
1384
1396
|
const renderView = createViewRouter([
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1397
|
+
// Routes go with the module, not just the nav entry: leaving them mounted
|
|
1398
|
+
// keeps a second product form reachable by URL.
|
|
1399
|
+
...config.modules.products ? [
|
|
1400
|
+
{ id: "products-list", render: () => /* @__PURE__ */ jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
|
|
1401
|
+
{ id: "products-new", render: () => /* @__PURE__ */ jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
|
|
1402
|
+
{ id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) }
|
|
1403
|
+
] : [],
|
|
1388
1404
|
{ id: "stock-entry", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
|
|
1389
1405
|
{ id: "stock-exit", render: () => /* @__PURE__ */ jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
|
|
1390
1406
|
{ id: "stock-history", render: () => /* @__PURE__ */ jsx(MovementHistoryView, { onViewDetail: (id) => navigate(`stock-detail:${id}`) }) },
|
|
@@ -1725,6 +1741,17 @@ function createMockInventoryProvider() {
|
|
|
1725
1741
|
return provider;
|
|
1726
1742
|
}
|
|
1727
1743
|
|
|
1744
|
+
// src/data/tables.ts
|
|
1745
|
+
var T = {
|
|
1746
|
+
stockLocations: "plg_inventory_stock_locations",
|
|
1747
|
+
stockMovements: "plg_inventory_stock_movements",
|
|
1748
|
+
stockPositions: "plg_inventory_stock_positions",
|
|
1749
|
+
recipes: "plg_inventory_recipes",
|
|
1750
|
+
recipeIngredients: "plg_inventory_recipe_ingredients",
|
|
1751
|
+
measurementUnits: "plg_inventory_measurement_units",
|
|
1752
|
+
productCategories: "plg_inventory_product_categories"
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1728
1755
|
// src/data/supabase.ts
|
|
1729
1756
|
function getTenantId() {
|
|
1730
1757
|
return getActiveTenantId();
|
|
@@ -1773,10 +1800,10 @@ function createSupabaseInventoryProvider() {
|
|
|
1773
1800
|
function getClients() {
|
|
1774
1801
|
const supabase = getSupabaseClientOptional();
|
|
1775
1802
|
if (!supabase) throw new Error("Supabase not initialized");
|
|
1776
|
-
return { core: supabase
|
|
1803
|
+
return { core: supabase, pub: supabase };
|
|
1777
1804
|
}
|
|
1778
1805
|
const provider = {
|
|
1779
|
-
// --- Products (
|
|
1806
|
+
// --- Products (public.products) ---
|
|
1780
1807
|
async getProducts(query) {
|
|
1781
1808
|
const { core} = getClients();
|
|
1782
1809
|
let qb = core.from("products").select("*", { count: "exact" });
|
|
@@ -1867,7 +1894,7 @@ function createSupabaseInventoryProvider() {
|
|
|
1867
1894
|
if (m.destinationLocationId && !m.destinationLocationName) locationIds.add(m.destinationLocationId);
|
|
1868
1895
|
}
|
|
1869
1896
|
if (locationIds.size > 0) {
|
|
1870
|
-
const { data: locs } = await pub.from(
|
|
1897
|
+
const { data: locs } = await pub.from(T.stockLocations).select("id, name").in("id", [...locationIds]);
|
|
1871
1898
|
const locMap = new Map((locs ?? []).map((l) => [l.id, l.name]));
|
|
1872
1899
|
for (const m of movements) {
|
|
1873
1900
|
if (m.stockLocationId && !m.stockLocationName) m.stockLocationName = locMap.get(m.stockLocationId);
|
|
@@ -1888,7 +1915,7 @@ function createSupabaseInventoryProvider() {
|
|
|
1888
1915
|
total_cost: unitCost * input.quantity,
|
|
1889
1916
|
movement_date: input.movementDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1890
1917
|
};
|
|
1891
|
-
const { data, error } = await pub.from(
|
|
1918
|
+
const { data, error } = await pub.from(T.stockMovements).insert(row).select().single();
|
|
1892
1919
|
if (error) throw new Error(error.message);
|
|
1893
1920
|
const { data: product } = await core.from("products").select("id, name, stock").eq("id", input.productId).single();
|
|
1894
1921
|
if (product) {
|
|
@@ -1900,11 +1927,11 @@ function createSupabaseInventoryProvider() {
|
|
|
1900
1927
|
if (input.stockLocationId) {
|
|
1901
1928
|
try {
|
|
1902
1929
|
const posDelta = input.movementType === "entry" ? input.quantity : -input.quantity;
|
|
1903
|
-
const { data: existing } = await pub.from(
|
|
1930
|
+
const { data: existing } = await pub.from(T.stockPositions).select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.stockLocationId).maybeSingle();
|
|
1904
1931
|
if (existing) {
|
|
1905
|
-
await pub.from(
|
|
1932
|
+
await pub.from(T.stockPositions).update({ quantity: (existing.quantity ?? 0) + posDelta, unit_cost: input.unitCost ?? 0 }).eq("id", existing.id);
|
|
1906
1933
|
} else {
|
|
1907
|
-
await pub.from(
|
|
1934
|
+
await pub.from(T.stockPositions).insert({
|
|
1908
1935
|
tenant_id: tenantId,
|
|
1909
1936
|
product_id: input.productId,
|
|
1910
1937
|
stock_location_id: input.stockLocationId,
|
|
@@ -1915,11 +1942,11 @@ function createSupabaseInventoryProvider() {
|
|
|
1915
1942
|
});
|
|
1916
1943
|
}
|
|
1917
1944
|
if (input.movementType === "transfer" && input.destinationLocationId) {
|
|
1918
|
-
const { data: destExisting } = await pub.from(
|
|
1945
|
+
const { data: destExisting } = await pub.from(T.stockPositions).select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.destinationLocationId).maybeSingle();
|
|
1919
1946
|
if (destExisting) {
|
|
1920
|
-
await pub.from(
|
|
1947
|
+
await pub.from(T.stockPositions).update({ quantity: (destExisting.quantity ?? 0) + input.quantity, unit_cost: input.unitCost ?? 0 }).eq("id", destExisting.id);
|
|
1921
1948
|
} else {
|
|
1922
|
-
await pub.from(
|
|
1949
|
+
await pub.from(T.stockPositions).insert({
|
|
1923
1950
|
tenant_id: tenantId,
|
|
1924
1951
|
product_id: input.productId,
|
|
1925
1952
|
stock_location_id: input.destinationLocationId,
|
|
@@ -1937,11 +1964,11 @@ function createSupabaseInventoryProvider() {
|
|
|
1937
1964
|
const movement = snakeToCamel(data);
|
|
1938
1965
|
movement.productName = product?.name;
|
|
1939
1966
|
if (input.stockLocationId) {
|
|
1940
|
-
const { data: loc } = await pub.from(
|
|
1967
|
+
const { data: loc } = await pub.from(T.stockLocations).select("name").eq("id", input.stockLocationId).single();
|
|
1941
1968
|
movement.stockLocationName = loc?.name;
|
|
1942
1969
|
}
|
|
1943
1970
|
if (input.destinationLocationId) {
|
|
1944
|
-
const { data: loc } = await pub.from(
|
|
1971
|
+
const { data: loc } = await pub.from(T.stockLocations).select("name").eq("id", input.destinationLocationId).single();
|
|
1945
1972
|
movement.destinationLocationName = loc?.name;
|
|
1946
1973
|
}
|
|
1947
1974
|
return movement;
|
|
@@ -1949,44 +1976,44 @@ function createSupabaseInventoryProvider() {
|
|
|
1949
1976
|
// --- Stock Positions ---
|
|
1950
1977
|
async getPositions(productId) {
|
|
1951
1978
|
const { pub } = getClients();
|
|
1952
|
-
const { data } = await pub.from(
|
|
1979
|
+
const { data } = await pub.from(T.stockPositions).select("*").eq("product_id", productId);
|
|
1953
1980
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1954
1981
|
},
|
|
1955
1982
|
// --- Stock Locations ---
|
|
1956
1983
|
async getLocations() {
|
|
1957
1984
|
const { pub } = getClients();
|
|
1958
|
-
const { data } = await pub.from(
|
|
1985
|
+
const { data } = await pub.from(T.stockLocations).select("*").eq("is_active", true).order("name");
|
|
1959
1986
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1960
1987
|
},
|
|
1961
1988
|
async createLocation(input) {
|
|
1962
1989
|
const { pub } = getClients();
|
|
1963
1990
|
const tenantId = getTenantId();
|
|
1964
|
-
const { data } = await pub.from(
|
|
1991
|
+
const { data } = await pub.from(T.stockLocations).insert({ ...camelToSnake(input), tenant_id: tenantId }).select().single();
|
|
1965
1992
|
return snakeToCamel(data);
|
|
1966
1993
|
},
|
|
1967
1994
|
// --- Recipes ---
|
|
1968
1995
|
async getRecipes() {
|
|
1969
1996
|
const { pub } = getClients();
|
|
1970
|
-
const { data } = await pub.from(
|
|
1997
|
+
const { data } = await pub.from(T.recipes).select("*").eq("is_active", true).order("name");
|
|
1971
1998
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1972
1999
|
},
|
|
1973
2000
|
async getRecipeById(id) {
|
|
1974
2001
|
const { pub } = getClients();
|
|
1975
|
-
const { data } = await pub.from(
|
|
2002
|
+
const { data } = await pub.from(T.recipes).select("*").eq("id", id).single();
|
|
1976
2003
|
return data ? snakeToCamel(data) : null;
|
|
1977
2004
|
},
|
|
1978
2005
|
async getRecipeIngredients(recipeId) {
|
|
1979
2006
|
const { pub } = getClients();
|
|
1980
|
-
const { data } = await pub.from(
|
|
2007
|
+
const { data } = await pub.from(T.recipeIngredients).select("*").eq("recipe_id", recipeId).order("display_order");
|
|
1981
2008
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1982
2009
|
},
|
|
1983
2010
|
async createRecipe(input) {
|
|
1984
2011
|
const { pub } = getClients();
|
|
1985
2012
|
const tenantId = getTenantId();
|
|
1986
2013
|
const { ingredients, ...recipeData } = input;
|
|
1987
|
-
const { data: recipe } = await pub.from(
|
|
2014
|
+
const { data: recipe } = await pub.from(T.recipes).insert({ ...camelToSnake(recipeData), tenant_id: tenantId }).select().single();
|
|
1988
2015
|
if (recipe && ingredients.length > 0) {
|
|
1989
|
-
await pub.from(
|
|
2016
|
+
await pub.from(T.recipeIngredients).insert(
|
|
1990
2017
|
ingredients.map((ing) => ({ ...camelToSnake(ing), recipe_id: recipe.id, tenant_id: tenantId }))
|
|
1991
2018
|
);
|
|
1992
2019
|
}
|
|
@@ -2002,7 +2029,7 @@ function createSupabaseInventoryProvider() {
|
|
|
2002
2029
|
const totalValue = items.reduce((sum, p) => sum + (p.stock ?? 0) * (p.price ?? 0), 0);
|
|
2003
2030
|
const weekAgo = /* @__PURE__ */ new Date();
|
|
2004
2031
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
2005
|
-
const { data: movements } = await pub.from(
|
|
2032
|
+
const { data: movements } = await pub.from(T.stockMovements).select("movement_type").gte("movement_date", weekAgo.toISOString().slice(0, 10));
|
|
2006
2033
|
const movs = movements ?? [];
|
|
2007
2034
|
const movementsByType = { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 };
|
|
2008
2035
|
for (const m of movs) movementsByType[m.movement_type]++;
|
|
@@ -2125,8 +2152,7 @@ var supplierEntity = {
|
|
|
2125
2152
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2126
2153
|
],
|
|
2127
2154
|
data: {
|
|
2128
|
-
table: "
|
|
2129
|
-
schema: "saas_core",
|
|
2155
|
+
table: "people",
|
|
2130
2156
|
tenantScoped: true,
|
|
2131
2157
|
filters: { kind: "supplier" },
|
|
2132
2158
|
defaults: { kind: "supplier" }
|
|
@@ -2143,7 +2169,7 @@ var measurementUnitEntity = {
|
|
|
2143
2169
|
{ key: "abbreviation", label: "Abbreviation", type: "text", required: true, showInTable: true },
|
|
2144
2170
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2145
2171
|
],
|
|
2146
|
-
data: { table:
|
|
2172
|
+
data: { table: T.measurementUnits, tenantScoped: true }
|
|
2147
2173
|
};
|
|
2148
2174
|
var productCategoryEntity = {
|
|
2149
2175
|
name: "Product Category",
|
|
@@ -2156,7 +2182,7 @@ var productCategoryEntity = {
|
|
|
2156
2182
|
{ key: "parentId", label: "Parent", type: "text", showInTable: false },
|
|
2157
2183
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2158
2184
|
],
|
|
2159
|
-
data: { table:
|
|
2185
|
+
data: { table: T.productCategories, tenantScoped: true }
|
|
2160
2186
|
};
|
|
2161
2187
|
var stockLocationEntity = {
|
|
2162
2188
|
name: "Stock Location",
|
|
@@ -2169,7 +2195,7 @@ var stockLocationEntity = {
|
|
|
2169
2195
|
{ key: "description", label: "Description", type: "textarea", showInTable: true },
|
|
2170
2196
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2171
2197
|
],
|
|
2172
|
-
data: { table:
|
|
2198
|
+
data: { table: T.stockLocations, tenantScoped: true }
|
|
2173
2199
|
};
|
|
2174
2200
|
var inventoryRegistries = [
|
|
2175
2201
|
// --- Editable ---
|
|
@@ -2731,6 +2757,7 @@ var DEFAULT_PRODUCT_TYPES = [
|
|
|
2731
2757
|
function resolveConfig(options) {
|
|
2732
2758
|
return {
|
|
2733
2759
|
modules: {
|
|
2760
|
+
products: options?.modules?.products !== false,
|
|
2734
2761
|
recipes: options?.modules?.recipes !== false,
|
|
2735
2762
|
stockLocations: options?.modules?.stockLocations !== false,
|
|
2736
2763
|
batchTracking: options?.modules?.batchTracking ?? false
|
|
@@ -2764,6 +2791,40 @@ function createInventoryPlugin(options) {
|
|
|
2764
2791
|
{ id: "inventory", label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
2765
2792
|
...config.modules.recipes ? [{ id: "inventory.recipes", label: config.labels.recipes ?? "Recipes", group: config.labels.pageTitle }] : []
|
|
2766
2793
|
],
|
|
2794
|
+
queryEntities: [
|
|
2795
|
+
{
|
|
2796
|
+
key: "inventory:products",
|
|
2797
|
+
writable: true,
|
|
2798
|
+
entity: {
|
|
2799
|
+
name: "Product",
|
|
2800
|
+
namePlural: "Products",
|
|
2801
|
+
icon: "Package",
|
|
2802
|
+
limitKey: "products",
|
|
2803
|
+
permission: { feature: "inventory", action: "read" },
|
|
2804
|
+
fields: [
|
|
2805
|
+
{ key: "name", label: "Name", type: "text", required: true, searchable: true },
|
|
2806
|
+
{ key: "kind", label: "Kind (sale/ingredient/asset)", type: "text" },
|
|
2807
|
+
{ key: "price", label: "Price", type: "number" },
|
|
2808
|
+
{ key: "cost", label: "Cost", type: "number" },
|
|
2809
|
+
{ key: "sku", label: "SKU", type: "text", searchable: true },
|
|
2810
|
+
{ key: "isActive", label: "Active", type: "boolean" },
|
|
2811
|
+
{ key: "createdAt", label: "Created at", type: "text" }
|
|
2812
|
+
],
|
|
2813
|
+
data: {
|
|
2814
|
+
table: "products",
|
|
2815
|
+
tenantScoped: true,
|
|
2816
|
+
archetype: "product",
|
|
2817
|
+
archetypeKind: "sale",
|
|
2818
|
+
searchColumns: ["name", "sku"],
|
|
2819
|
+
defaults: { kind: "sale", status: "active", is_active: true }
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
],
|
|
2824
|
+
declaredLimits: [
|
|
2825
|
+
{ key: "products", label: "Products", table: "products" },
|
|
2826
|
+
...config.modules.recipes ? [{ key: "recipes", label: config.labels.recipes ?? "Recipes", table: "plg_inventory_recipes" }] : []
|
|
2827
|
+
],
|
|
2767
2828
|
navigation: [
|
|
2768
2829
|
{
|
|
2769
2830
|
section: options?.navSection ?? "main",
|