@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.
Files changed (38) hide show
  1. package/README.md +2 -0
  2. package/dist/InventoryContext.d.ts +2 -0
  3. package/dist/InventoryContext.d.ts.map +1 -1
  4. package/dist/InventoryPage.d.ts.map +1 -1
  5. package/dist/components/InventoryGeneralSettings.d.ts.map +1 -1
  6. package/dist/data/supabase.d.ts.map +1 -1
  7. package/dist/data/tables.d.ts +10 -0
  8. package/dist/data/tables.d.ts.map +1 -0
  9. package/dist/index.cjs +111 -50
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +8 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +112 -51
  14. package/dist/index.js.map +1 -1
  15. package/dist/migrations/index.d.ts +9 -0
  16. package/dist/migrations/index.d.ts.map +1 -0
  17. package/dist/views/ProductCrudForm.d.ts.map +1 -1
  18. package/dist/views/ProductListView.d.ts.map +1 -1
  19. package/dist/views/RecipeFormView.d.ts.map +1 -1
  20. package/dist/views/RecipesView.d.ts.map +1 -1
  21. package/package.json +10 -4
  22. package/src/InventoryContext.tsx +2 -0
  23. package/src/InventoryPage.tsx +13 -7
  24. package/src/README.md +1 -1
  25. package/src/components/InventoryGeneralSettings.tsx +21 -8
  26. package/src/data/supabase.ts +24 -23
  27. package/src/data/tables.ts +10 -0
  28. package/src/index.ts +43 -0
  29. package/src/migrations/000_plg_rename.sql +27 -0
  30. package/src/migrations/001_inventory_base.sql +27 -27
  31. package/src/migrations/002_recipes.sql +13 -13
  32. package/src/migrations/003_measurement_units.sql +4 -4
  33. package/src/migrations/index.ts +161 -0
  34. package/src/registries.ts +5 -5
  35. package/src/views/ProductCrudForm.tsx +6 -1
  36. package/src/views/ProductListView.tsx +1 -0
  37. package/src/views/RecipeFormView.tsx +6 -0
  38. package/src/views/RecipesView.tsx +6 -3
package/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [![npm](https://img.shields.io/npm/v/@fayz-ai/plugin-inventory.svg)](https://www.npmjs.com/package/@fayz-ai/plugin-inventory)
6
6
  [![license](https://img.shields.io/npm/l/@fayz-ai/plugin-inventory.svg)](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,qBA+GA"}
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;AAIzB,wBAAgB,wBAAwB,sBAqBvC"}
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;AA4DpD,wBAAgB,+BAA+B,IAAI,qBAAqB,CA2SvE"}
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: true, onChange: () => {
1230
- } }),
1231
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: true, onChange: () => {
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: false, onChange: () => {
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: false, onChange: () => {
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
- { id: "products-list", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
1392
- { id: "products-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
1393
- { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) },
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.schema("saas_core"), pub: supabase };
1809
+ return { core: supabase, pub: supabase };
1783
1810
  }
1784
1811
  const provider = {
1785
- // --- Products (saas_core.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("stock_locations").select("id, name").in("id", [...locationIds]);
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("stock_movements").insert(row).select().single();
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("stock_positions").select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.stockLocationId).maybeSingle();
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("stock_positions").update({ quantity: (existing.quantity ?? 0) + posDelta, unit_cost: input.unitCost ?? 0 }).eq("id", existing.id);
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("stock_positions").insert({
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("stock_positions").select("id, quantity").eq("product_id", input.productId).eq("stock_location_id", input.destinationLocationId).maybeSingle();
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("stock_positions").update({ quantity: (destExisting.quantity ?? 0) + input.quantity, unit_cost: input.unitCost ?? 0 }).eq("id", destExisting.id);
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("stock_positions").insert({
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("stock_locations").select("name").eq("id", input.stockLocationId).single();
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("stock_locations").select("name").eq("id", input.destinationLocationId).single();
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("stock_positions").select("*").eq("product_id", productId);
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("stock_locations").select("*").eq("is_active", true).order("name");
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("stock_locations").insert({ ...camelToSnake(input), tenant_id: tenantId }).select().single();
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("recipes").select("*").eq("is_active", true).order("name");
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("recipes").select("*").eq("id", id).single();
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("recipe_ingredients").select("*").eq("recipe_id", recipeId).order("display_order");
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("recipes").insert({ ...camelToSnake(recipeData), tenant_id: tenantId }).select().single();
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("recipe_ingredients").insert(
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("stock_movements").select("movement_type").gte("movement_date", weekAgo.toISOString().slice(0, 10));
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: "persons",
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: "measurement_units", tenantScoped: true }
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: "product_categories", tenantScoped: true }
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: "stock_locations", tenantScoped: true }
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",