@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/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@fayz-ai/plugin-inventory)
|
|
6
6
|
[](https://github.com/FayaLabs/fayz-sdk/blob/main/LICENSE)
|
|
7
7
|
|
|
8
|
+
**Status:** beta — pre-1.0. Core surface is stable; some backend facets (see `PLUGIN_PATTERNS.md`) are still landing. APIs may change before 1.0.
|
|
9
|
+
|
|
8
10
|
Inventory is the universal organ — a salon counts product, a restaurant counts ingredients, a clinic counts supplies. `plugin-inventory` is that organ as a plugin: a product catalog, stock entry/exit/history, optional recipes (bill of materials), and a dashboard that surfaces stock value and what's running low. It's `scope: universal`, so it snaps into any vertical.
|
|
9
11
|
|
|
10
12
|
It ships real data out of the box: a Supabase provider with an automatic mock fallback, so a fresh app demos immediately and a configured one persists for real. Recipes, stock locations, and batch tracking are opt-in modules; product types, currency, and labels are configurable. The host dashboard gets inventory KPI widgets, and the AI assistant can answer "what's running low?" The intent: stop rebuilding stock control per app — compose it from one plugin into a real SaaS.
|
|
@@ -3,6 +3,8 @@ import type { InventoryPluginLabels } from './index';
|
|
|
3
3
|
import type { InventoryDataProvider } from './data/types';
|
|
4
4
|
import type { InventoryUIState } from './store';
|
|
5
5
|
export interface InventoryModules {
|
|
6
|
+
/** Product catalogue screens. Off when the host registers products elsewhere. */
|
|
7
|
+
products: boolean;
|
|
6
8
|
recipes: boolean;
|
|
7
9
|
stockLocations: boolean;
|
|
8
10
|
batchTracking: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InventoryContext.d.ts","sourceRoot":"","sources":["../src/InventoryContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAA;IACjC,SAAS,EAAE,cAAc,EAAE,CAAA;CAC5B;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
|
1
|
+
{"version":3,"file":"InventoryContext.d.ts","sourceRoot":"","sources":["../src/InventoryContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAA;IACjC,SAAS,EAAE,cAAc,EAAE,CAAA;CAC5B;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InventoryPage.d.ts","sourceRoot":"","sources":["../src/InventoryPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,eAAe,CAAA;AA+CzE,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IACrE,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACjC,
|
|
1
|
+
{"version":3,"file":"InventoryPage.d.ts","sourceRoot":"","sources":["../src/InventoryPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,eAAe,CAAA;AA+CzE,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IACrE,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACjC,qBAqHA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InventoryGeneralSettings.d.ts","sourceRoot":"","sources":["../../src/components/InventoryGeneralSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"InventoryGeneralSettings.d.ts","sourceRoot":"","sources":["../../src/components/InventoryGeneralSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAgBzB,wBAAgB,wBAAwB,sBAsBvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AA6DpD,wBAAgB,+BAA+B,IAAI,qBAAqB,CA2SvE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const T: {
|
|
2
|
+
readonly stockLocations: "plg_inventory_stock_locations";
|
|
3
|
+
readonly stockMovements: "plg_inventory_stock_movements";
|
|
4
|
+
readonly stockPositions: "plg_inventory_stock_positions";
|
|
5
|
+
readonly recipes: "plg_inventory_recipes";
|
|
6
|
+
readonly recipeIngredients: "plg_inventory_recipe_ingredients";
|
|
7
|
+
readonly measurementUnits: "plg_inventory_measurement_units";
|
|
8
|
+
readonly productCategories: "plg_inventory_product_categories";
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=tables.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../../src/data/tables.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,CAAC;;;;;;;;CAQJ,CAAA"}
|
package/dist/index.cjs
CHANGED
|
@@ -173,7 +173,8 @@ function ProductListView({ onNew, onEdit }) {
|
|
|
173
173
|
onFacetChange: (_field, value) => setTypeFilter(value),
|
|
174
174
|
onNew,
|
|
175
175
|
addLabel: t2("inventory.productList.newProduct"),
|
|
176
|
-
onRowClick: (row) => onEdit?.(row.id)
|
|
176
|
+
onRowClick: (row) => onEdit?.(row.id),
|
|
177
|
+
feature: "inventory"
|
|
177
178
|
}
|
|
178
179
|
);
|
|
179
180
|
}
|
|
@@ -182,6 +183,7 @@ function ProductCrudForm({ editId, onSaved }) {
|
|
|
182
183
|
const { productTypes, currency } = useInventoryConfig();
|
|
183
184
|
const provider = useInventoryProvider();
|
|
184
185
|
const createProduct = useInventoryStore((s) => s.createProduct);
|
|
186
|
+
const guardProducts = saas.useLimitGuard("products");
|
|
185
187
|
const isEdit = !!editId;
|
|
186
188
|
const [initialData, setInitialData] = React2__default.default.useState(isEdit ? null : {});
|
|
187
189
|
const [headerName, setHeaderName] = React2__default.default.useState("");
|
|
@@ -233,7 +235,9 @@ function ProductCrudForm({ editId, onSaved }) {
|
|
|
233
235
|
if (isEdit && editId) {
|
|
234
236
|
await provider.updateProduct(editId, payload);
|
|
235
237
|
} else {
|
|
238
|
+
if (await guardProducts() === "blocked") return;
|
|
236
239
|
await createProduct(payload);
|
|
240
|
+
saas.invalidateLimit("products");
|
|
237
241
|
}
|
|
238
242
|
onSaved?.();
|
|
239
243
|
}
|
|
@@ -809,11 +813,11 @@ function RecipesView({ onNew, onView }) {
|
|
|
809
813
|
{
|
|
810
814
|
title: t2("inventory.recipes.title"),
|
|
811
815
|
subtitle: t2("inventory.recipes.productionFormulas", { count: String(recipes.length) }),
|
|
812
|
-
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: [
|
|
816
|
+
actions: onNew && /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "inventory", action: "create", children: /* @__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: [
|
|
813
817
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
|
|
814
818
|
" ",
|
|
815
819
|
t2("inventory.recipes.newRecipe")
|
|
816
|
-
] })
|
|
820
|
+
] }) })
|
|
817
821
|
}
|
|
818
822
|
),
|
|
819
823
|
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: [
|
|
@@ -872,6 +876,7 @@ function RecipeFormView({ onSaved }) {
|
|
|
872
876
|
const t2 = core.useTranslation();
|
|
873
877
|
const provider = useInventoryProvider();
|
|
874
878
|
const createRecipe = useInventoryStore((s) => s.createRecipe);
|
|
879
|
+
const guardRecipes = saas.useLimitGuard("recipes");
|
|
875
880
|
const [name, setName] = React2.useState("");
|
|
876
881
|
const [description, setDescription] = React2.useState("");
|
|
877
882
|
const [productId, setProductId] = React2.useState("");
|
|
@@ -895,6 +900,7 @@ function RecipeFormView({ onSaved }) {
|
|
|
895
900
|
ui.toast.error(t2("common.formIncomplete"));
|
|
896
901
|
return;
|
|
897
902
|
}
|
|
903
|
+
if (await guardRecipes() === "blocked") return;
|
|
898
904
|
setSaving(true);
|
|
899
905
|
try {
|
|
900
906
|
const recipe = await createRecipe({
|
|
@@ -912,6 +918,7 @@ function RecipeFormView({ onSaved }) {
|
|
|
912
918
|
notes: i.notes || void 0
|
|
913
919
|
}))
|
|
914
920
|
});
|
|
921
|
+
saas.invalidateLimit("recipes");
|
|
915
922
|
onSaved?.(recipe.id);
|
|
916
923
|
} finally {
|
|
917
924
|
setSaving(false);
|
|
@@ -1222,28 +1229,31 @@ function RecipeDetailView({ recipeId, onBack }) {
|
|
|
1222
1229
|
] })
|
|
1223
1230
|
] });
|
|
1224
1231
|
}
|
|
1232
|
+
var INVENTORY_DEFAULTS = {
|
|
1233
|
+
lowStockAlerts: true,
|
|
1234
|
+
requireReason: true,
|
|
1235
|
+
autoDeduct: false,
|
|
1236
|
+
requireSku: false,
|
|
1237
|
+
allowNegative: false,
|
|
1238
|
+
lowStockEmail: false,
|
|
1239
|
+
expiryWarnings: true
|
|
1240
|
+
};
|
|
1225
1241
|
function InventoryGeneralSettings() {
|
|
1226
1242
|
const t2 = core.useTranslation();
|
|
1243
|
+
const s = saas.useTenantPluginSettings("inventory", INVENTORY_DEFAULTS);
|
|
1227
1244
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
1228
1245
|
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
|
|
1229
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked:
|
|
1230
|
-
|
|
1231
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.
|
|
1232
|
-
} }),
|
|
1233
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: false, onChange: () => {
|
|
1234
|
-
} })
|
|
1246
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: s.get("lowStockAlerts"), onChange: (v) => s.set("lowStockAlerts", v) }),
|
|
1247
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: s.get("requireReason"), onChange: (v) => s.set("requireReason", v) }),
|
|
1248
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: s.get("autoDeduct"), onChange: (v) => s.set("autoDeduct", v) })
|
|
1235
1249
|
] }),
|
|
1236
1250
|
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.products"), description: t2("inventory.settings.productsDesc"), children: [
|
|
1237
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked:
|
|
1238
|
-
|
|
1239
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: false, onChange: () => {
|
|
1240
|
-
} })
|
|
1251
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: s.get("requireSku"), onChange: (v) => s.set("requireSku", v) }),
|
|
1252
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: s.get("allowNegative"), onChange: (v) => s.set("allowNegative", v) })
|
|
1241
1253
|
] }),
|
|
1242
1254
|
/* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.notifications"), description: t2("inventory.settings.notificationsDesc"), children: [
|
|
1243
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked:
|
|
1244
|
-
|
|
1245
|
-
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: true, onChange: () => {
|
|
1246
|
-
} })
|
|
1255
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: s.get("lowStockEmail"), onChange: (v) => s.set("lowStockEmail", v) }),
|
|
1256
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: s.get("expiryWarnings"), onChange: (v) => s.set("expiryWarnings", v) })
|
|
1247
1257
|
] })
|
|
1248
1258
|
] });
|
|
1249
1259
|
}
|
|
@@ -1285,7 +1295,7 @@ function InventoryOnboarding({ onComplete }) {
|
|
|
1285
1295
|
function buildNav(config, view, navigate, t2) {
|
|
1286
1296
|
const items = [
|
|
1287
1297
|
{ id: "dashboard", label: t2("inventory.nav.dashboard"), icon: "BarChart3", active: view === "dashboard", onClick: () => navigate("dashboard") },
|
|
1288
|
-
{
|
|
1298
|
+
...config.modules.products ? [{
|
|
1289
1299
|
id: "products",
|
|
1290
1300
|
label: t2("inventory.nav.products"),
|
|
1291
1301
|
icon: "Package",
|
|
@@ -1294,7 +1304,7 @@ function buildNav(config, view, navigate, t2) {
|
|
|
1294
1304
|
{ id: "products-new", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
|
|
1295
1305
|
{ id: "products-list", label: t2("inventory.nav.list"), active: view === "products-list", onClick: () => navigate("products-list") }
|
|
1296
1306
|
]
|
|
1297
|
-
},
|
|
1307
|
+
}] : [],
|
|
1298
1308
|
{
|
|
1299
1309
|
id: "stock",
|
|
1300
1310
|
label: t2("inventory.nav.stock"),
|
|
@@ -1345,13 +1355,15 @@ function InventoryPage({ config, provider, store, registries }) {
|
|
|
1345
1355
|
const nav = buildNav(config, view, navigate, t2);
|
|
1346
1356
|
const quickActions = React2.useMemo(() => {
|
|
1347
1357
|
const actions = [
|
|
1348
|
-
|
|
1358
|
+
// Dropped with the catalogue module: offering "new product" while the
|
|
1359
|
+
// screen is gone would dead-end the user.
|
|
1360
|
+
...config.modules.products ? [{
|
|
1349
1361
|
id: "new-product",
|
|
1350
1362
|
label: t2("inventory.quickActions.newProduct"),
|
|
1351
1363
|
icon: "Package",
|
|
1352
1364
|
description: t2("inventory.quickActions.newProductDesc"),
|
|
1353
1365
|
action: () => navigate("products-new")
|
|
1354
|
-
},
|
|
1366
|
+
}] : [],
|
|
1355
1367
|
{
|
|
1356
1368
|
id: "stock-entry",
|
|
1357
1369
|
label: t2("inventory.quickActions.stockEntry"),
|
|
@@ -1388,9 +1400,13 @@ function InventoryPage({ config, provider, store, registries }) {
|
|
|
1388
1400
|
] }) }) });
|
|
1389
1401
|
}
|
|
1390
1402
|
const renderView = saas.createViewRouter([
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1403
|
+
// Routes go with the module, not just the nav entry: leaving them mounted
|
|
1404
|
+
// keeps a second product form reachable by URL.
|
|
1405
|
+
...config.modules.products ? [
|
|
1406
|
+
{ id: "products-list", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
|
|
1407
|
+
{ id: "products-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
|
|
1408
|
+
{ id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) }
|
|
1409
|
+
] : [],
|
|
1394
1410
|
{ id: "stock-entry", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
|
|
1395
1411
|
{ id: "stock-exit", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
|
|
1396
1412
|
{ id: "stock-history", render: () => /* @__PURE__ */ jsxRuntime.jsx(MovementHistoryView, { onViewDetail: (id) => navigate(`stock-detail:${id}`) }) },
|
|
@@ -1731,6 +1747,17 @@ function createMockInventoryProvider() {
|
|
|
1731
1747
|
return provider;
|
|
1732
1748
|
}
|
|
1733
1749
|
|
|
1750
|
+
// src/data/tables.ts
|
|
1751
|
+
var T = {
|
|
1752
|
+
stockLocations: "plg_inventory_stock_locations",
|
|
1753
|
+
stockMovements: "plg_inventory_stock_movements",
|
|
1754
|
+
stockPositions: "plg_inventory_stock_positions",
|
|
1755
|
+
recipes: "plg_inventory_recipes",
|
|
1756
|
+
recipeIngredients: "plg_inventory_recipe_ingredients",
|
|
1757
|
+
measurementUnits: "plg_inventory_measurement_units",
|
|
1758
|
+
productCategories: "plg_inventory_product_categories"
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1734
1761
|
// src/data/supabase.ts
|
|
1735
1762
|
function getTenantId() {
|
|
1736
1763
|
return core.getActiveTenantId();
|
|
@@ -1779,10 +1806,10 @@ function createSupabaseInventoryProvider() {
|
|
|
1779
1806
|
function getClients() {
|
|
1780
1807
|
const supabase = core.getSupabaseClientOptional();
|
|
1781
1808
|
if (!supabase) throw new Error("Supabase not initialized");
|
|
1782
|
-
return { core: supabase
|
|
1809
|
+
return { core: supabase, pub: supabase };
|
|
1783
1810
|
}
|
|
1784
1811
|
const provider = {
|
|
1785
|
-
// --- Products (
|
|
1812
|
+
// --- Products (public.products) ---
|
|
1786
1813
|
async getProducts(query) {
|
|
1787
1814
|
const { core} = getClients();
|
|
1788
1815
|
let qb = core.from("products").select("*", { count: "exact" });
|
|
@@ -1873,7 +1900,7 @@ function createSupabaseInventoryProvider() {
|
|
|
1873
1900
|
if (m.destinationLocationId && !m.destinationLocationName) locationIds.add(m.destinationLocationId);
|
|
1874
1901
|
}
|
|
1875
1902
|
if (locationIds.size > 0) {
|
|
1876
|
-
const { data: locs } = await pub.from(
|
|
1903
|
+
const { data: locs } = await pub.from(T.stockLocations).select("id, name").in("id", [...locationIds]);
|
|
1877
1904
|
const locMap = new Map((locs ?? []).map((l) => [l.id, l.name]));
|
|
1878
1905
|
for (const m of movements) {
|
|
1879
1906
|
if (m.stockLocationId && !m.stockLocationName) m.stockLocationName = locMap.get(m.stockLocationId);
|
|
@@ -1894,7 +1921,7 @@ function createSupabaseInventoryProvider() {
|
|
|
1894
1921
|
total_cost: unitCost * input.quantity,
|
|
1895
1922
|
movement_date: input.movementDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1896
1923
|
};
|
|
1897
|
-
const { data, error } = await pub.from(
|
|
1924
|
+
const { data, error } = await pub.from(T.stockMovements).insert(row).select().single();
|
|
1898
1925
|
if (error) throw new Error(error.message);
|
|
1899
1926
|
const { data: product } = await core.from("products").select("id, name, stock").eq("id", input.productId).single();
|
|
1900
1927
|
if (product) {
|
|
@@ -1906,11 +1933,11 @@ function createSupabaseInventoryProvider() {
|
|
|
1906
1933
|
if (input.stockLocationId) {
|
|
1907
1934
|
try {
|
|
1908
1935
|
const posDelta = input.movementType === "entry" ? input.quantity : -input.quantity;
|
|
1909
|
-
const { data: existing } = await pub.from(
|
|
1936
|
+
const { data: existing } = await pub.from(T.stockPositions).select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.stockLocationId).maybeSingle();
|
|
1910
1937
|
if (existing) {
|
|
1911
|
-
await pub.from(
|
|
1938
|
+
await pub.from(T.stockPositions).update({ quantity: (existing.quantity ?? 0) + posDelta, unit_cost: input.unitCost ?? 0 }).eq("id", existing.id);
|
|
1912
1939
|
} else {
|
|
1913
|
-
await pub.from(
|
|
1940
|
+
await pub.from(T.stockPositions).insert({
|
|
1914
1941
|
tenant_id: tenantId,
|
|
1915
1942
|
product_id: input.productId,
|
|
1916
1943
|
stock_location_id: input.stockLocationId,
|
|
@@ -1921,11 +1948,11 @@ function createSupabaseInventoryProvider() {
|
|
|
1921
1948
|
});
|
|
1922
1949
|
}
|
|
1923
1950
|
if (input.movementType === "transfer" && input.destinationLocationId) {
|
|
1924
|
-
const { data: destExisting } = await pub.from(
|
|
1951
|
+
const { data: destExisting } = await pub.from(T.stockPositions).select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.destinationLocationId).maybeSingle();
|
|
1925
1952
|
if (destExisting) {
|
|
1926
|
-
await pub.from(
|
|
1953
|
+
await pub.from(T.stockPositions).update({ quantity: (destExisting.quantity ?? 0) + input.quantity, unit_cost: input.unitCost ?? 0 }).eq("id", destExisting.id);
|
|
1927
1954
|
} else {
|
|
1928
|
-
await pub.from(
|
|
1955
|
+
await pub.from(T.stockPositions).insert({
|
|
1929
1956
|
tenant_id: tenantId,
|
|
1930
1957
|
product_id: input.productId,
|
|
1931
1958
|
stock_location_id: input.destinationLocationId,
|
|
@@ -1943,11 +1970,11 @@ function createSupabaseInventoryProvider() {
|
|
|
1943
1970
|
const movement = snakeToCamel(data);
|
|
1944
1971
|
movement.productName = product?.name;
|
|
1945
1972
|
if (input.stockLocationId) {
|
|
1946
|
-
const { data: loc } = await pub.from(
|
|
1973
|
+
const { data: loc } = await pub.from(T.stockLocations).select("name").eq("id", input.stockLocationId).single();
|
|
1947
1974
|
movement.stockLocationName = loc?.name;
|
|
1948
1975
|
}
|
|
1949
1976
|
if (input.destinationLocationId) {
|
|
1950
|
-
const { data: loc } = await pub.from(
|
|
1977
|
+
const { data: loc } = await pub.from(T.stockLocations).select("name").eq("id", input.destinationLocationId).single();
|
|
1951
1978
|
movement.destinationLocationName = loc?.name;
|
|
1952
1979
|
}
|
|
1953
1980
|
return movement;
|
|
@@ -1955,44 +1982,44 @@ function createSupabaseInventoryProvider() {
|
|
|
1955
1982
|
// --- Stock Positions ---
|
|
1956
1983
|
async getPositions(productId) {
|
|
1957
1984
|
const { pub } = getClients();
|
|
1958
|
-
const { data } = await pub.from(
|
|
1985
|
+
const { data } = await pub.from(T.stockPositions).select("*").eq("product_id", productId);
|
|
1959
1986
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1960
1987
|
},
|
|
1961
1988
|
// --- Stock Locations ---
|
|
1962
1989
|
async getLocations() {
|
|
1963
1990
|
const { pub } = getClients();
|
|
1964
|
-
const { data } = await pub.from(
|
|
1991
|
+
const { data } = await pub.from(T.stockLocations).select("*").eq("is_active", true).order("name");
|
|
1965
1992
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1966
1993
|
},
|
|
1967
1994
|
async createLocation(input) {
|
|
1968
1995
|
const { pub } = getClients();
|
|
1969
1996
|
const tenantId = getTenantId();
|
|
1970
|
-
const { data } = await pub.from(
|
|
1997
|
+
const { data } = await pub.from(T.stockLocations).insert({ ...camelToSnake(input), tenant_id: tenantId }).select().single();
|
|
1971
1998
|
return snakeToCamel(data);
|
|
1972
1999
|
},
|
|
1973
2000
|
// --- Recipes ---
|
|
1974
2001
|
async getRecipes() {
|
|
1975
2002
|
const { pub } = getClients();
|
|
1976
|
-
const { data } = await pub.from(
|
|
2003
|
+
const { data } = await pub.from(T.recipes).select("*").eq("is_active", true).order("name");
|
|
1977
2004
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1978
2005
|
},
|
|
1979
2006
|
async getRecipeById(id) {
|
|
1980
2007
|
const { pub } = getClients();
|
|
1981
|
-
const { data } = await pub.from(
|
|
2008
|
+
const { data } = await pub.from(T.recipes).select("*").eq("id", id).single();
|
|
1982
2009
|
return data ? snakeToCamel(data) : null;
|
|
1983
2010
|
},
|
|
1984
2011
|
async getRecipeIngredients(recipeId) {
|
|
1985
2012
|
const { pub } = getClients();
|
|
1986
|
-
const { data } = await pub.from(
|
|
2013
|
+
const { data } = await pub.from(T.recipeIngredients).select("*").eq("recipe_id", recipeId).order("display_order");
|
|
1987
2014
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
1988
2015
|
},
|
|
1989
2016
|
async createRecipe(input) {
|
|
1990
2017
|
const { pub } = getClients();
|
|
1991
2018
|
const tenantId = getTenantId();
|
|
1992
2019
|
const { ingredients, ...recipeData } = input;
|
|
1993
|
-
const { data: recipe } = await pub.from(
|
|
2020
|
+
const { data: recipe } = await pub.from(T.recipes).insert({ ...camelToSnake(recipeData), tenant_id: tenantId }).select().single();
|
|
1994
2021
|
if (recipe && ingredients.length > 0) {
|
|
1995
|
-
await pub.from(
|
|
2022
|
+
await pub.from(T.recipeIngredients).insert(
|
|
1996
2023
|
ingredients.map((ing) => ({ ...camelToSnake(ing), recipe_id: recipe.id, tenant_id: tenantId }))
|
|
1997
2024
|
);
|
|
1998
2025
|
}
|
|
@@ -2008,7 +2035,7 @@ function createSupabaseInventoryProvider() {
|
|
|
2008
2035
|
const totalValue = items.reduce((sum, p) => sum + (p.stock ?? 0) * (p.price ?? 0), 0);
|
|
2009
2036
|
const weekAgo = /* @__PURE__ */ new Date();
|
|
2010
2037
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
2011
|
-
const { data: movements } = await pub.from(
|
|
2038
|
+
const { data: movements } = await pub.from(T.stockMovements).select("movement_type").gte("movement_date", weekAgo.toISOString().slice(0, 10));
|
|
2012
2039
|
const movs = movements ?? [];
|
|
2013
2040
|
const movementsByType = { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 };
|
|
2014
2041
|
for (const m of movs) movementsByType[m.movement_type]++;
|
|
@@ -2131,8 +2158,7 @@ var supplierEntity = {
|
|
|
2131
2158
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2132
2159
|
],
|
|
2133
2160
|
data: {
|
|
2134
|
-
table: "
|
|
2135
|
-
schema: "saas_core",
|
|
2161
|
+
table: "people",
|
|
2136
2162
|
tenantScoped: true,
|
|
2137
2163
|
filters: { kind: "supplier" },
|
|
2138
2164
|
defaults: { kind: "supplier" }
|
|
@@ -2149,7 +2175,7 @@ var measurementUnitEntity = {
|
|
|
2149
2175
|
{ key: "abbreviation", label: "Abbreviation", type: "text", required: true, showInTable: true },
|
|
2150
2176
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2151
2177
|
],
|
|
2152
|
-
data: { table:
|
|
2178
|
+
data: { table: T.measurementUnits, tenantScoped: true }
|
|
2153
2179
|
};
|
|
2154
2180
|
var productCategoryEntity = {
|
|
2155
2181
|
name: "Product Category",
|
|
@@ -2162,7 +2188,7 @@ var productCategoryEntity = {
|
|
|
2162
2188
|
{ key: "parentId", label: "Parent", type: "text", showInTable: false },
|
|
2163
2189
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2164
2190
|
],
|
|
2165
|
-
data: { table:
|
|
2191
|
+
data: { table: T.productCategories, tenantScoped: true }
|
|
2166
2192
|
};
|
|
2167
2193
|
var stockLocationEntity = {
|
|
2168
2194
|
name: "Stock Location",
|
|
@@ -2175,7 +2201,7 @@ var stockLocationEntity = {
|
|
|
2175
2201
|
{ key: "description", label: "Description", type: "textarea", showInTable: true },
|
|
2176
2202
|
{ key: "isActive", label: "Active", type: "boolean", showInTable: true, defaultValue: true }
|
|
2177
2203
|
],
|
|
2178
|
-
data: { table:
|
|
2204
|
+
data: { table: T.stockLocations, tenantScoped: true }
|
|
2179
2205
|
};
|
|
2180
2206
|
var inventoryRegistries = [
|
|
2181
2207
|
// --- Editable ---
|
|
@@ -2737,6 +2763,7 @@ var DEFAULT_PRODUCT_TYPES = [
|
|
|
2737
2763
|
function resolveConfig(options) {
|
|
2738
2764
|
return {
|
|
2739
2765
|
modules: {
|
|
2766
|
+
products: options?.modules?.products !== false,
|
|
2740
2767
|
recipes: options?.modules?.recipes !== false,
|
|
2741
2768
|
stockLocations: options?.modules?.stockLocations !== false,
|
|
2742
2769
|
batchTracking: options?.modules?.batchTracking ?? false
|
|
@@ -2770,6 +2797,40 @@ function createInventoryPlugin(options) {
|
|
|
2770
2797
|
{ id: "inventory", label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
2771
2798
|
...config.modules.recipes ? [{ id: "inventory.recipes", label: config.labels.recipes ?? "Recipes", group: config.labels.pageTitle }] : []
|
|
2772
2799
|
],
|
|
2800
|
+
queryEntities: [
|
|
2801
|
+
{
|
|
2802
|
+
key: "inventory:products",
|
|
2803
|
+
writable: true,
|
|
2804
|
+
entity: {
|
|
2805
|
+
name: "Product",
|
|
2806
|
+
namePlural: "Products",
|
|
2807
|
+
icon: "Package",
|
|
2808
|
+
limitKey: "products",
|
|
2809
|
+
permission: { feature: "inventory", action: "read" },
|
|
2810
|
+
fields: [
|
|
2811
|
+
{ key: "name", label: "Name", type: "text", required: true, searchable: true },
|
|
2812
|
+
{ key: "kind", label: "Kind (sale/ingredient/asset)", type: "text" },
|
|
2813
|
+
{ key: "price", label: "Price", type: "number" },
|
|
2814
|
+
{ key: "cost", label: "Cost", type: "number" },
|
|
2815
|
+
{ key: "sku", label: "SKU", type: "text", searchable: true },
|
|
2816
|
+
{ key: "isActive", label: "Active", type: "boolean" },
|
|
2817
|
+
{ key: "createdAt", label: "Created at", type: "text" }
|
|
2818
|
+
],
|
|
2819
|
+
data: {
|
|
2820
|
+
table: "products",
|
|
2821
|
+
tenantScoped: true,
|
|
2822
|
+
archetype: "product",
|
|
2823
|
+
archetypeKind: "sale",
|
|
2824
|
+
searchColumns: ["name", "sku"],
|
|
2825
|
+
defaults: { kind: "sale", status: "active", is_active: true }
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
],
|
|
2830
|
+
declaredLimits: [
|
|
2831
|
+
{ key: "products", label: "Products", table: "products" },
|
|
2832
|
+
...config.modules.recipes ? [{ key: "recipes", label: config.labels.recipes ?? "Recipes", table: "plg_inventory_recipes" }] : []
|
|
2833
|
+
],
|
|
2773
2834
|
navigation: [
|
|
2774
2835
|
{
|
|
2775
2836
|
section: options?.navSection ?? "main",
|