@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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const MIGRATION_000_PLG_RENAME = "-- 000_plg_rename.sql \u2014 rename legacy inventory tables to plg_inventory_* for pools\n-- provisioned before the industry-pool rename. Guarded: fires only when the legacy\n-- name exists and the target does not, so fresh pools skip every branch.\nDO $$\nBEGIN\n IF to_regclass('public.stock_locations') IS NOT NULL AND to_regclass('public.plg_inventory_stock_locations') IS NULL THEN\n ALTER TABLE public.stock_locations RENAME TO plg_inventory_stock_locations;\n END IF;\n IF to_regclass('public.stock_movements') IS NOT NULL AND to_regclass('public.plg_inventory_stock_movements') IS NULL THEN\n ALTER TABLE public.stock_movements RENAME TO plg_inventory_stock_movements;\n END IF;\n IF to_regclass('public.stock_positions') IS NOT NULL AND to_regclass('public.plg_inventory_stock_positions') IS NULL THEN\n ALTER TABLE public.stock_positions RENAME TO plg_inventory_stock_positions;\n END IF;\n IF to_regclass('public.recipes') IS NOT NULL AND to_regclass('public.plg_inventory_recipes') IS NULL THEN\n ALTER TABLE public.recipes RENAME TO plg_inventory_recipes;\n END IF;\n IF to_regclass('public.recipe_ingredients') IS NOT NULL AND to_regclass('public.plg_inventory_recipe_ingredients') IS NULL THEN\n ALTER TABLE public.recipe_ingredients RENAME TO plg_inventory_recipe_ingredients;\n END IF;\n IF to_regclass('public.measurement_units') IS NOT NULL AND to_regclass('public.plg_inventory_measurement_units') IS NULL THEN\n ALTER TABLE public.measurement_units RENAME TO plg_inventory_measurement_units;\n END IF;\n IF to_regclass('public.product_categories') IS NOT NULL AND to_regclass('public.plg_inventory_product_categories') IS NULL THEN\n ALTER TABLE public.product_categories RENAME TO plg_inventory_product_categories;\n END IF;\nEND $$;\n";
|
|
2
|
+
export declare const MIGRATION_001_INVENTORY_BASE = "-- Inventory Plugin: Base Tables\n-- Products use public.products archetype directly\n-- These are plugin-specific extension tables\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_product_categories (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n parent_id uuid REFERENCES public.plg_inventory_product_categories(id),\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_inventory_product_categories ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_product_categories_tenant ON public.plg_inventory_product_categories(tenant_id);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_stock_locations (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n description text,\n is_active boolean NOT NULL DEFAULT true,\n unit_id uuid,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_inventory_stock_locations ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_locations_tenant ON public.plg_inventory_stock_locations(tenant_id);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_stock_movements (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n product_id uuid NOT NULL REFERENCES public.products(id),\n quantity numeric(14,4) NOT NULL,\n movement_type text NOT NULL,\n unit_cost numeric(14,2) DEFAULT 0,\n total_cost numeric(14,2) DEFAULT 0,\n stock_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),\n destination_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),\n supplier_id uuid REFERENCES public.people(id),\n document_number text,\n reason text,\n notes text,\n movement_date date NOT NULL DEFAULT CURRENT_DATE,\n user_id uuid,\n batch_number text,\n expiration_date date,\n metadata jsonb DEFAULT '{}'::jsonb,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_inventory_stock_movements ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_tenant ON public.plg_inventory_stock_movements(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_product ON public.plg_inventory_stock_movements(product_id);\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_date ON public.plg_inventory_stock_movements(tenant_id, movement_date);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_stock_positions (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n product_id uuid NOT NULL REFERENCES public.products(id),\n quantity numeric(14,4) NOT NULL,\n unit_cost numeric(14,2) DEFAULT 0,\n stock_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),\n batch_number text,\n expiration_date date,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_inventory_stock_positions ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_positions_tenant ON public.plg_inventory_stock_positions(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_positions_product ON public.plg_inventory_stock_positions(product_id);\n";
|
|
3
|
+
export declare const MIGRATION_002_RECIPES = "-- Inventory Plugin: Recipes & Technical Specs\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_recipes (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n description text,\n product_id uuid REFERENCES public.products(id),\n yield_quantity numeric(14,4) DEFAULT 1,\n yield_unit_id uuid,\n preparation_time_minutes integer,\n instructions text,\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_inventory_recipes ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_recipes_tenant ON public.plg_inventory_recipes(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_recipes_product ON public.plg_inventory_recipes(product_id);\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_recipe_ingredients (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n recipe_id uuid NOT NULL REFERENCES public.plg_inventory_recipes(id) ON DELETE CASCADE,\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n product_id uuid NOT NULL REFERENCES public.products(id),\n quantity numeric(14,4) NOT NULL,\n unit_id uuid,\n display_order integer DEFAULT 0,\n notes text,\n created_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_inventory_recipe_ingredients ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_recipe_ingredients_tenant ON public.plg_inventory_recipe_ingredients(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_recipe_ingredients_recipe ON public.plg_inventory_recipe_ingredients(recipe_id);\n";
|
|
4
|
+
export declare const MIGRATION_003_MEASUREMENT_UNITS = "-- Inventory Plugin: Measurement Units\n\nCREATE TABLE IF NOT EXISTS public.plg_inventory_measurement_units (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n name text NOT NULL,\n abbreviation text NOT NULL,\n is_active boolean NOT NULL DEFAULT true,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_inventory_measurement_units ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_inventory_measurement_units_tenant ON public.plg_inventory_measurement_units(tenant_id);\n";
|
|
5
|
+
export declare const MIGRATIONS: Array<{
|
|
6
|
+
id: string;
|
|
7
|
+
sql: string;
|
|
8
|
+
}>;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,wBAAwB,qwDA2BpC,CAAA;AAED,eAAO,MAAM,4BAA4B,+9GAqExC,CAAA;AAED,eAAO,MAAM,qBAAqB,+rDAkCjC,CAAA;AAED,eAAO,MAAM,+BAA+B,uoBAa3C,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAKzD,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductCrudForm.d.ts","sourceRoot":"","sources":["../../src/views/ProductCrudForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAgBzB,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"ProductCrudForm.d.ts","sourceRoot":"","sources":["../../src/views/ProductCrudForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAgBzB,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,qBAuF7F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProductListView.d.ts","sourceRoot":"","sources":["../../src/views/ProductListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAA;AAW3D,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IACjD,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;IAClB,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAC9B,
|
|
1
|
+
{"version":3,"file":"ProductListView.d.ts","sourceRoot":"","sources":["../../src/views/ProductListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAA;AAW3D,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IACjD,KAAK,CAAC,EAAE,MAAM,IAAI,CAAA;IAClB,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAC9B,qBA2CA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecipeFormView.d.ts","sourceRoot":"","sources":["../../src/views/RecipeFormView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"RecipeFormView.d.ts","sourceRoot":"","sources":["../../src/views/RecipeFormView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAA;AAuBvC,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,qBAyN9E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RecipesView.d.ts","sourceRoot":"","sources":["../../src/views/RecipesView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"RecipesView.d.ts","sourceRoot":"","sources":["../../src/views/RecipesView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoB,MAAM,OAAO,CAAA;AA6BxC,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,qBA4EnG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fayz-ai/plugin-inventory",
|
|
3
|
-
"
|
|
3
|
+
"fayz": {
|
|
4
|
+
"status": "beta"
|
|
5
|
+
},
|
|
6
|
+
"version": "0.8.1",
|
|
4
7
|
"description": "Fayz SDK — plugin-inventory plugin",
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "./dist/index.cjs",
|
|
@@ -26,9 +29,9 @@
|
|
|
26
29
|
"lucide-react": "^0.400.0",
|
|
27
30
|
"zustand": "^4.5.0",
|
|
28
31
|
"@tanstack/react-table": "^8.20.0",
|
|
29
|
-
"@fayz-ai/
|
|
30
|
-
"@fayz-ai/ui": "^0.
|
|
31
|
-
"@fayz-ai/
|
|
32
|
+
"@fayz-ai/saas": "^0.8.2",
|
|
33
|
+
"@fayz-ai/ui": "^0.8.1",
|
|
34
|
+
"@fayz-ai/core": "^0.8.2"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@types/react": "^18.3.0",
|
|
@@ -43,6 +46,9 @@
|
|
|
43
46
|
"fayz-plugin",
|
|
44
47
|
"fayz-sdk"
|
|
45
48
|
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
46
52
|
"scripts": {
|
|
47
53
|
"build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
|
|
48
54
|
"dev": "tsup --watch",
|
package/src/InventoryContext.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import type { InventoryDataProvider } from './data/types'
|
|
|
4
4
|
import type { InventoryUIState } from './store'
|
|
5
5
|
|
|
6
6
|
export interface InventoryModules {
|
|
7
|
+
/** Product catalogue screens. Off when the host registers products elsewhere. */
|
|
8
|
+
products: boolean
|
|
7
9
|
recipes: boolean
|
|
8
10
|
stockLocations: boolean
|
|
9
11
|
batchTracking: boolean
|
package/src/InventoryPage.tsx
CHANGED
|
@@ -21,13 +21,13 @@ import { InventoryOnboarding } from './components/InventoryOnboarding'
|
|
|
21
21
|
function buildNav(config: ResolvedInventoryConfig, view: string, navigate: (v: string) => void, t: (key: string) => string): ModuleNavItem[] {
|
|
22
22
|
const items: ModuleNavItem[] = [
|
|
23
23
|
{ id: 'dashboard', label: t('inventory.nav.dashboard'), icon: 'BarChart3', active: view === 'dashboard', onClick: () => navigate('dashboard') },
|
|
24
|
-
{
|
|
24
|
+
...(config.modules.products ? [{
|
|
25
25
|
id: 'products', label: t('inventory.nav.products'), icon: 'Package', active: view.startsWith('products'),
|
|
26
26
|
children: [
|
|
27
27
|
{ id: 'products-new', label: t('inventory.nav.new'), active: view === 'products-new', onClick: () => navigate('products-new') },
|
|
28
28
|
{ id: 'products-list', label: t('inventory.nav.list'), active: view === 'products-list', onClick: () => navigate('products-list') },
|
|
29
29
|
],
|
|
30
|
-
},
|
|
30
|
+
} as ModuleNavItem] : []),
|
|
31
31
|
{
|
|
32
32
|
id: 'stock', label: t('inventory.nav.stock'), icon: 'ArrowUpCircle',
|
|
33
33
|
children: [
|
|
@@ -76,13 +76,15 @@ export function InventoryPage({ config, provider, store, registries }: {
|
|
|
76
76
|
|
|
77
77
|
const quickActions = useMemo<PluginQuickAction[]>(() => {
|
|
78
78
|
const actions: PluginQuickAction[] = [
|
|
79
|
-
|
|
79
|
+
// Dropped with the catalogue module: offering "new product" while the
|
|
80
|
+
// screen is gone would dead-end the user.
|
|
81
|
+
...(config.modules.products ? [{
|
|
80
82
|
id: 'new-product',
|
|
81
83
|
label: t('inventory.quickActions.newProduct'),
|
|
82
84
|
icon: 'Package',
|
|
83
85
|
description: t('inventory.quickActions.newProductDesc'),
|
|
84
86
|
action: () => navigate('products-new'),
|
|
85
|
-
},
|
|
87
|
+
} as PluginQuickAction] : []),
|
|
86
88
|
{
|
|
87
89
|
id: 'stock-entry',
|
|
88
90
|
label: t('inventory.quickActions.stockEntry'),
|
|
@@ -128,9 +130,13 @@ export function InventoryPage({ config, provider, store, registries }: {
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
const renderView = createViewRouter([
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
// Routes go with the module, not just the nav entry: leaving them mounted
|
|
134
|
+
// keeps a second product form reachable by URL.
|
|
135
|
+
...(config.modules.products ? [
|
|
136
|
+
{ id: 'products-list', render: () => <ProductListView onNew={() => navigate('products-new')} onEdit={(id: string) => navigate(`products-edit:${id}`)} /> },
|
|
137
|
+
{ id: 'products-new', render: () => <ProductCrudForm onSaved={() => navigate('products-list')} /> },
|
|
138
|
+
{ id: 'products-edit', render: ({ id }: { id?: string }) => <ProductCrudForm editId={id!} onSaved={() => navigate('products-list')} /> },
|
|
139
|
+
] : []),
|
|
134
140
|
{ id: 'stock-entry', render: () => <StockMovementView defaultType="entry" onSaved={() => navigate('stock-history')} /> },
|
|
135
141
|
{ id: 'stock-exit', render: () => <StockMovementView defaultType="exit" onSaved={() => navigate('stock-history')} /> },
|
|
136
142
|
{ id: 'stock-history', render: () => <MovementHistoryView onViewDetail={(id) => navigate(`stock-detail:${id}`)} /> },
|
package/src/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Product catalog, stock management, movement tracking, and recipe/production form
|
|
|
7
7
|
```typescript
|
|
8
8
|
import { createInventoryPlugin } from '@fayz-ai/saas-core/plugins/inventory'
|
|
9
9
|
|
|
10
|
-
// In your
|
|
10
|
+
// In your defineSaas config:
|
|
11
11
|
plugins: [
|
|
12
12
|
createInventoryPlugin({
|
|
13
13
|
currency: { code: 'BRL', locale: 'pt-BR', symbol: 'R$' },
|
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { SettingsGroup, ToggleRow } from '@fayz-ai/saas'
|
|
2
|
+
import { SettingsGroup, ToggleRow, useTenantPluginSettings } from '@fayz-ai/saas'
|
|
3
3
|
import { useTranslation } from '@fayz-ai/core'
|
|
4
4
|
|
|
5
|
+
// Per-tenant defaults = the values that used to be hardcoded here. Persisted +
|
|
6
|
+
// re-hydrated per tenant; behavioural consumption is a follow-up.
|
|
7
|
+
const INVENTORY_DEFAULTS = {
|
|
8
|
+
lowStockAlerts: true,
|
|
9
|
+
requireReason: true,
|
|
10
|
+
autoDeduct: false,
|
|
11
|
+
requireSku: false,
|
|
12
|
+
allowNegative: false,
|
|
13
|
+
lowStockEmail: false,
|
|
14
|
+
expiryWarnings: true,
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export function InventoryGeneralSettings() {
|
|
6
18
|
const t = useTranslation()
|
|
19
|
+
const s = useTenantPluginSettings('inventory', INVENTORY_DEFAULTS)
|
|
7
20
|
return (
|
|
8
21
|
<div className="space-y-4">
|
|
9
22
|
<SettingsGroup title={t('inventory.settings.stockManagement')} description={t('inventory.settings.stockManagementDesc')}>
|
|
10
|
-
<ToggleRow label={t('inventory.settings.lowStockAlerts')} description={t('inventory.settings.lowStockAlertsDesc')} checked={
|
|
11
|
-
<ToggleRow label={t('inventory.settings.requireReason')} description={t('inventory.settings.requireReasonDesc')} checked={
|
|
12
|
-
<ToggleRow label={t('inventory.settings.autoDeduct')} description={t('inventory.settings.autoDeductDesc')} checked={
|
|
23
|
+
<ToggleRow label={t('inventory.settings.lowStockAlerts')} description={t('inventory.settings.lowStockAlertsDesc')} checked={s.get('lowStockAlerts')} onChange={(v) => s.set('lowStockAlerts', v)} />
|
|
24
|
+
<ToggleRow label={t('inventory.settings.requireReason')} description={t('inventory.settings.requireReasonDesc')} checked={s.get('requireReason')} onChange={(v) => s.set('requireReason', v)} />
|
|
25
|
+
<ToggleRow label={t('inventory.settings.autoDeduct')} description={t('inventory.settings.autoDeductDesc')} checked={s.get('autoDeduct')} onChange={(v) => s.set('autoDeduct', v)} />
|
|
13
26
|
</SettingsGroup>
|
|
14
27
|
|
|
15
28
|
<SettingsGroup title={t('inventory.settings.products')} description={t('inventory.settings.productsDesc')}>
|
|
16
|
-
<ToggleRow label={t('inventory.settings.requireSku')} description={t('inventory.settings.requireSkuDesc')} checked={
|
|
17
|
-
<ToggleRow label={t('inventory.settings.allowNegative')} description={t('inventory.settings.allowNegativeDesc')} checked={
|
|
29
|
+
<ToggleRow label={t('inventory.settings.requireSku')} description={t('inventory.settings.requireSkuDesc')} checked={s.get('requireSku')} onChange={(v) => s.set('requireSku', v)} />
|
|
30
|
+
<ToggleRow label={t('inventory.settings.allowNegative')} description={t('inventory.settings.allowNegativeDesc')} checked={s.get('allowNegative')} onChange={(v) => s.set('allowNegative', v)} />
|
|
18
31
|
</SettingsGroup>
|
|
19
32
|
|
|
20
33
|
<SettingsGroup title={t('inventory.settings.notifications')} description={t('inventory.settings.notificationsDesc')}>
|
|
21
|
-
<ToggleRow label={t('inventory.settings.lowStockEmail')} description={t('inventory.settings.lowStockEmailDesc')} checked={
|
|
22
|
-
<ToggleRow label={t('inventory.settings.expiryWarnings')} description={t('inventory.settings.expiryWarningsDesc')} checked={
|
|
34
|
+
<ToggleRow label={t('inventory.settings.lowStockEmail')} description={t('inventory.settings.lowStockEmailDesc')} checked={s.get('lowStockEmail')} onChange={(v) => s.set('lowStockEmail', v)} />
|
|
35
|
+
<ToggleRow label={t('inventory.settings.expiryWarnings')} description={t('inventory.settings.expiryWarningsDesc')} checked={s.get('expiryWarnings')} onChange={(v) => s.set('expiryWarnings', v)} />
|
|
23
36
|
</SettingsGroup>
|
|
24
37
|
</div>
|
|
25
38
|
)
|
package/src/data/supabase.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from '../types'
|
|
9
9
|
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
|
|
10
10
|
import { getInventoryTenantId } from '../lib/tenant'
|
|
11
|
+
import { T } from './tables'
|
|
11
12
|
|
|
12
13
|
function getTenantId(): string | undefined {
|
|
13
14
|
// Local override wins; else use the app's active tenant so writes pass RLS.
|
|
@@ -33,7 +34,7 @@ function camelToSnake(obj: Record<string, unknown>): Record<string, unknown> {
|
|
|
33
34
|
return result
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** Map a
|
|
37
|
+
/** Map a public.products row to our Product type */
|
|
37
38
|
function mapProductRow(row: Record<string, any>): Product {
|
|
38
39
|
const meta = row.metadata ?? {}
|
|
39
40
|
return {
|
|
@@ -64,11 +65,11 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
64
65
|
function getClients() {
|
|
65
66
|
const supabase = getSupabaseClientOptional() as any
|
|
66
67
|
if (!supabase) throw new Error('Supabase not initialized')
|
|
67
|
-
return { core: supabase
|
|
68
|
+
return { core: supabase, pub: supabase }
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
const provider: InventoryDataProvider = {
|
|
71
|
-
// --- Products (
|
|
72
|
+
// --- Products (public.products) ---
|
|
72
73
|
async getProducts(query: ProductQuery): Promise<PaginatedResult<Product>> {
|
|
73
74
|
const { core, pub } = getClients()
|
|
74
75
|
let qb = core.from('products').select('*', { count: 'exact' })
|
|
@@ -133,7 +134,7 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
133
134
|
// --- Stock Movements (via view with product join — single query) ---
|
|
134
135
|
async getMovements(query: MovementQuery): Promise<PaginatedResult<StockMovement>> {
|
|
135
136
|
const { pub } = getClients()
|
|
136
|
-
// Single query via v_stock_movements view (JOINs with
|
|
137
|
+
// Single query via v_stock_movements view (JOINs with public.products)
|
|
137
138
|
let qb = pub.from('v_stock_movements').select('*', { count: 'exact' })
|
|
138
139
|
if (query.productId) qb = qb.eq('product_id', query.productId)
|
|
139
140
|
if (query.movementType) {
|
|
@@ -169,7 +170,7 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
169
170
|
if (m.destinationLocationId && !m.destinationLocationName) locationIds.add(m.destinationLocationId)
|
|
170
171
|
}
|
|
171
172
|
if (locationIds.size > 0) {
|
|
172
|
-
const { data: locs } = await pub.from(
|
|
173
|
+
const { data: locs } = await pub.from(T.stockLocations).select('id, name').in('id', [...locationIds])
|
|
173
174
|
const locMap = new Map((locs ?? []).map((l: any) => [l.id, l.name]))
|
|
174
175
|
for (const m of movements) {
|
|
175
176
|
if (m.stockLocationId && !m.stockLocationName) m.stockLocationName = locMap.get(m.stockLocationId)
|
|
@@ -192,7 +193,7 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
192
193
|
total_cost: unitCost * input.quantity,
|
|
193
194
|
movement_date: input.movementDate ?? new Date().toISOString().slice(0, 10),
|
|
194
195
|
}
|
|
195
|
-
const { data, error } = await pub.from(
|
|
196
|
+
const { data, error } = await pub.from(T.stockMovements).insert(row).select().single()
|
|
196
197
|
if (error) throw new Error(error.message)
|
|
197
198
|
|
|
198
199
|
// Fetch product name + current stock for the response and stock update
|
|
@@ -210,18 +211,18 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
210
211
|
if (input.stockLocationId) {
|
|
211
212
|
try {
|
|
212
213
|
const posDelta = (input.movementType === 'entry') ? input.quantity : -(input.quantity)
|
|
213
|
-
const { data: existing } = await pub.from(
|
|
214
|
+
const { data: existing } = await pub.from(T.stockPositions)
|
|
214
215
|
.select('id, quantity')
|
|
215
216
|
.eq('product_id', input.productId)
|
|
216
217
|
.eq('stock_location_id', input.stockLocationId)
|
|
217
218
|
.maybeSingle()
|
|
218
219
|
|
|
219
220
|
if (existing) {
|
|
220
|
-
await pub.from(
|
|
221
|
+
await pub.from(T.stockPositions)
|
|
221
222
|
.update({ quantity: (existing.quantity ?? 0) + posDelta, unit_cost: input.unitCost ?? 0 })
|
|
222
223
|
.eq('id', existing.id)
|
|
223
224
|
} else {
|
|
224
|
-
await pub.from(
|
|
225
|
+
await pub.from(T.stockPositions).insert({
|
|
225
226
|
tenant_id: tenantId,
|
|
226
227
|
product_id: input.productId,
|
|
227
228
|
stock_location_id: input.stockLocationId,
|
|
@@ -234,18 +235,18 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
234
235
|
|
|
235
236
|
// For transfers, also update the destination position
|
|
236
237
|
if (input.movementType === 'transfer' && input.destinationLocationId) {
|
|
237
|
-
const { data: destExisting } = await pub.from(
|
|
238
|
+
const { data: destExisting } = await pub.from(T.stockPositions)
|
|
238
239
|
.select('id, quantity')
|
|
239
240
|
.eq('product_id', input.productId)
|
|
240
241
|
.eq('stock_location_id', input.destinationLocationId)
|
|
241
242
|
.maybeSingle()
|
|
242
243
|
|
|
243
244
|
if (destExisting) {
|
|
244
|
-
await pub.from(
|
|
245
|
+
await pub.from(T.stockPositions)
|
|
245
246
|
.update({ quantity: (destExisting.quantity ?? 0) + input.quantity, unit_cost: input.unitCost ?? 0 })
|
|
246
247
|
.eq('id', destExisting.id)
|
|
247
248
|
} else {
|
|
248
|
-
await pub.from(
|
|
249
|
+
await pub.from(T.stockPositions).insert({
|
|
249
250
|
tenant_id: tenantId,
|
|
250
251
|
product_id: input.productId,
|
|
251
252
|
stock_location_id: input.destinationLocationId,
|
|
@@ -266,11 +267,11 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
266
267
|
movement.productName = product?.name
|
|
267
268
|
// Resolve location name
|
|
268
269
|
if (input.stockLocationId) {
|
|
269
|
-
const { data: loc } = await pub.from(
|
|
270
|
+
const { data: loc } = await pub.from(T.stockLocations).select('name').eq('id', input.stockLocationId).single()
|
|
270
271
|
movement.stockLocationName = loc?.name
|
|
271
272
|
}
|
|
272
273
|
if (input.destinationLocationId) {
|
|
273
|
-
const { data: loc } = await pub.from(
|
|
274
|
+
const { data: loc } = await pub.from(T.stockLocations).select('name').eq('id', input.destinationLocationId).single()
|
|
274
275
|
movement.destinationLocationName = loc?.name
|
|
275
276
|
}
|
|
276
277
|
return movement as StockMovement
|
|
@@ -279,40 +280,40 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
279
280
|
// --- Stock Positions ---
|
|
280
281
|
async getPositions(productId: string): Promise<StockPosition[]> {
|
|
281
282
|
const { pub } = getClients()
|
|
282
|
-
const { data } = await pub.from(
|
|
283
|
+
const { data } = await pub.from(T.stockPositions).select('*').eq('product_id', productId)
|
|
283
284
|
return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as StockPosition)
|
|
284
285
|
},
|
|
285
286
|
|
|
286
287
|
// --- Stock Locations ---
|
|
287
288
|
async getLocations(): Promise<StockLocation[]> {
|
|
288
289
|
const { pub } = getClients()
|
|
289
|
-
const { data } = await pub.from(
|
|
290
|
+
const { data } = await pub.from(T.stockLocations).select('*').eq('is_active', true).order('name')
|
|
290
291
|
return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as StockLocation)
|
|
291
292
|
},
|
|
292
293
|
|
|
293
294
|
async createLocation(input): Promise<StockLocation> {
|
|
294
295
|
const { pub } = getClients()
|
|
295
296
|
const tenantId = getTenantId()
|
|
296
|
-
const { data } = await pub.from(
|
|
297
|
+
const { data } = await pub.from(T.stockLocations).insert({ ...camelToSnake(input as any), tenant_id: tenantId }).select().single()
|
|
297
298
|
return snakeToCamel(data!) as unknown as StockLocation
|
|
298
299
|
},
|
|
299
300
|
|
|
300
301
|
// --- Recipes ---
|
|
301
302
|
async getRecipes(): Promise<Recipe[]> {
|
|
302
303
|
const { pub } = getClients()
|
|
303
|
-
const { data } = await pub.from(
|
|
304
|
+
const { data } = await pub.from(T.recipes).select('*').eq('is_active', true).order('name')
|
|
304
305
|
return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as Recipe)
|
|
305
306
|
},
|
|
306
307
|
|
|
307
308
|
async getRecipeById(id: string): Promise<Recipe | null> {
|
|
308
309
|
const { pub } = getClients()
|
|
309
|
-
const { data } = await pub.from(
|
|
310
|
+
const { data } = await pub.from(T.recipes).select('*').eq('id', id).single()
|
|
310
311
|
return data ? snakeToCamel(data) as unknown as Recipe : null
|
|
311
312
|
},
|
|
312
313
|
|
|
313
314
|
async getRecipeIngredients(recipeId: string): Promise<RecipeIngredient[]> {
|
|
314
315
|
const { pub } = getClients()
|
|
315
|
-
const { data } = await pub.from(
|
|
316
|
+
const { data } = await pub.from(T.recipeIngredients).select('*').eq('recipe_id', recipeId).order('display_order')
|
|
316
317
|
return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as RecipeIngredient)
|
|
317
318
|
},
|
|
318
319
|
|
|
@@ -320,9 +321,9 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
320
321
|
const { pub } = getClients()
|
|
321
322
|
const tenantId = getTenantId()
|
|
322
323
|
const { ingredients, ...recipeData } = input
|
|
323
|
-
const { data: recipe } = await pub.from(
|
|
324
|
+
const { data: recipe } = await pub.from(T.recipes).insert({ ...camelToSnake(recipeData as any), tenant_id: tenantId }).select().single()
|
|
324
325
|
if (recipe && ingredients.length > 0) {
|
|
325
|
-
await pub.from(
|
|
326
|
+
await pub.from(T.recipeIngredients).insert(
|
|
326
327
|
ingredients.map((ing) => ({ ...camelToSnake(ing as any), recipe_id: recipe.id, tenant_id: tenantId }))
|
|
327
328
|
)
|
|
328
329
|
}
|
|
@@ -340,7 +341,7 @@ export function createSupabaseInventoryProvider(): InventoryDataProvider {
|
|
|
340
341
|
|
|
341
342
|
const weekAgo = new Date()
|
|
342
343
|
weekAgo.setDate(weekAgo.getDate() - 7)
|
|
343
|
-
const { data: movements } = await pub.from(
|
|
344
|
+
const { data: movements } = await pub.from(T.stockMovements).select('movement_type').gte('movement_date', weekAgo.toISOString().slice(0, 10))
|
|
344
345
|
const movs = movements ?? []
|
|
345
346
|
const movementsByType: Record<MovementType, number> = { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 }
|
|
346
347
|
for (const m of movs) movementsByType[m.movement_type as MovementType]++
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Central physical-table-name registry for plugin-inventory.
|
|
2
|
+
export const T = {
|
|
3
|
+
stockLocations: 'plg_inventory_stock_locations',
|
|
4
|
+
stockMovements: 'plg_inventory_stock_movements',
|
|
5
|
+
stockPositions: 'plg_inventory_stock_positions',
|
|
6
|
+
recipes: 'plg_inventory_recipes',
|
|
7
|
+
recipeIngredients: 'plg_inventory_recipe_ingredients',
|
|
8
|
+
measurementUnits: 'plg_inventory_measurement_units',
|
|
9
|
+
productCategories: 'plg_inventory_product_categories',
|
|
10
|
+
} as const
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,14 @@ export interface InventoryPluginOptions {
|
|
|
37
37
|
recipes?: boolean
|
|
38
38
|
stockLocations?: boolean
|
|
39
39
|
batchTracking?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* The product catalogue screens (list + form). Defaults on. Turn OFF when the
|
|
42
|
+
* host already owns product registration elsewhere — an e-commerce app
|
|
43
|
+
* manages products in the shop plugin, and two competing product CRUDs is
|
|
44
|
+
* how the same product ends up entered twice, differently.
|
|
45
|
+
* Stock entry/exit/history stay available either way.
|
|
46
|
+
*/
|
|
47
|
+
products?: boolean
|
|
40
48
|
}
|
|
41
49
|
labels?: Partial<InventoryPluginLabels>
|
|
42
50
|
productTypes?: Array<{ value: string; label: string }>
|
|
@@ -85,6 +93,7 @@ const DEFAULT_PRODUCT_TYPES = [
|
|
|
85
93
|
function resolveConfig(options?: InventoryPluginOptions): ResolvedInventoryConfig {
|
|
86
94
|
return {
|
|
87
95
|
modules: {
|
|
96
|
+
products: options?.modules?.products !== false,
|
|
88
97
|
recipes: options?.modules?.recipes !== false,
|
|
89
98
|
stockLocations: options?.modules?.stockLocations !== false,
|
|
90
99
|
batchTracking: options?.modules?.batchTracking ?? false,
|
|
@@ -126,6 +135,40 @@ export function createInventoryPlugin(options?: InventoryPluginOptions): PluginM
|
|
|
126
135
|
{ id: 'inventory', label: config.labels.pageTitle, group: config.labels.pageTitle },
|
|
127
136
|
...(config.modules.recipes ? [{ id: 'inventory.recipes', label: config.labels.recipes ?? 'Recipes', group: config.labels.pageTitle }] : []),
|
|
128
137
|
],
|
|
138
|
+
queryEntities: [
|
|
139
|
+
{
|
|
140
|
+
key: 'inventory:products',
|
|
141
|
+
writable: true,
|
|
142
|
+
entity: {
|
|
143
|
+
name: 'Product',
|
|
144
|
+
namePlural: 'Products',
|
|
145
|
+
icon: 'Package',
|
|
146
|
+
limitKey: 'products',
|
|
147
|
+
permission: { feature: 'inventory', action: 'read' },
|
|
148
|
+
fields: [
|
|
149
|
+
{ key: 'name', label: 'Name', type: 'text', required: true, searchable: true },
|
|
150
|
+
{ key: 'kind', label: 'Kind (sale/ingredient/asset)', type: 'text' },
|
|
151
|
+
{ key: 'price', label: 'Price', type: 'number' },
|
|
152
|
+
{ key: 'cost', label: 'Cost', type: 'number' },
|
|
153
|
+
{ key: 'sku', label: 'SKU', type: 'text', searchable: true },
|
|
154
|
+
{ key: 'isActive', label: 'Active', type: 'boolean' },
|
|
155
|
+
{ key: 'createdAt', label: 'Created at', type: 'text' },
|
|
156
|
+
],
|
|
157
|
+
data: {
|
|
158
|
+
table: 'products',
|
|
159
|
+
tenantScoped: true,
|
|
160
|
+
archetype: 'product',
|
|
161
|
+
archetypeKind: 'sale',
|
|
162
|
+
searchColumns: ['name', 'sku'],
|
|
163
|
+
defaults: { kind: 'sale', status: 'active', is_active: true },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
declaredLimits: [
|
|
169
|
+
{ key: 'products', label: 'Products', table: 'products' },
|
|
170
|
+
...(config.modules.recipes ? [{ key: 'recipes', label: config.labels.recipes ?? 'Recipes', table: 'plg_inventory_recipes' }] : []),
|
|
171
|
+
],
|
|
129
172
|
navigation: [
|
|
130
173
|
{
|
|
131
174
|
section: options?.navSection ?? 'main',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- 000_plg_rename.sql — rename legacy inventory tables to plg_inventory_* for pools
|
|
2
|
+
-- provisioned before the industry-pool rename. Guarded: fires only when the legacy
|
|
3
|
+
-- name exists and the target does not, so fresh pools skip every branch.
|
|
4
|
+
DO $$
|
|
5
|
+
BEGIN
|
|
6
|
+
IF to_regclass('public.stock_locations') IS NOT NULL AND to_regclass('public.plg_inventory_stock_locations') IS NULL THEN
|
|
7
|
+
ALTER TABLE public.stock_locations RENAME TO plg_inventory_stock_locations;
|
|
8
|
+
END IF;
|
|
9
|
+
IF to_regclass('public.stock_movements') IS NOT NULL AND to_regclass('public.plg_inventory_stock_movements') IS NULL THEN
|
|
10
|
+
ALTER TABLE public.stock_movements RENAME TO plg_inventory_stock_movements;
|
|
11
|
+
END IF;
|
|
12
|
+
IF to_regclass('public.stock_positions') IS NOT NULL AND to_regclass('public.plg_inventory_stock_positions') IS NULL THEN
|
|
13
|
+
ALTER TABLE public.stock_positions RENAME TO plg_inventory_stock_positions;
|
|
14
|
+
END IF;
|
|
15
|
+
IF to_regclass('public.recipes') IS NOT NULL AND to_regclass('public.plg_inventory_recipes') IS NULL THEN
|
|
16
|
+
ALTER TABLE public.recipes RENAME TO plg_inventory_recipes;
|
|
17
|
+
END IF;
|
|
18
|
+
IF to_regclass('public.recipe_ingredients') IS NOT NULL AND to_regclass('public.plg_inventory_recipe_ingredients') IS NULL THEN
|
|
19
|
+
ALTER TABLE public.recipe_ingredients RENAME TO plg_inventory_recipe_ingredients;
|
|
20
|
+
END IF;
|
|
21
|
+
IF to_regclass('public.measurement_units') IS NOT NULL AND to_regclass('public.plg_inventory_measurement_units') IS NULL THEN
|
|
22
|
+
ALTER TABLE public.measurement_units RENAME TO plg_inventory_measurement_units;
|
|
23
|
+
END IF;
|
|
24
|
+
IF to_regclass('public.product_categories') IS NOT NULL AND to_regclass('public.plg_inventory_product_categories') IS NULL THEN
|
|
25
|
+
ALTER TABLE public.product_categories RENAME TO plg_inventory_product_categories;
|
|
26
|
+
END IF;
|
|
27
|
+
END $$;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
-- Inventory Plugin: Base Tables
|
|
2
|
-
-- Products use
|
|
2
|
+
-- Products use public.products archetype directly
|
|
3
3
|
-- These are plugin-specific extension tables
|
|
4
4
|
|
|
5
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
5
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_product_categories (
|
|
6
6
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
7
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
7
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
8
8
|
name text NOT NULL,
|
|
9
|
-
parent_id uuid REFERENCES public.
|
|
9
|
+
parent_id uuid REFERENCES public.plg_inventory_product_categories(id),
|
|
10
10
|
is_active boolean NOT NULL DEFAULT true,
|
|
11
11
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
12
12
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
13
13
|
);
|
|
14
|
-
ALTER TABLE public.
|
|
15
|
-
CREATE INDEX IF NOT EXISTS
|
|
14
|
+
ALTER TABLE public.plg_inventory_product_categories ENABLE ROW LEVEL SECURITY;
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_plg_inventory_product_categories_tenant ON public.plg_inventory_product_categories(tenant_id);
|
|
16
16
|
|
|
17
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
17
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_stock_locations (
|
|
18
18
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
19
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
19
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
20
20
|
name text NOT NULL,
|
|
21
21
|
description text,
|
|
22
22
|
is_active boolean NOT NULL DEFAULT true,
|
|
@@ -24,20 +24,20 @@ CREATE TABLE IF NOT EXISTS public.stock_locations (
|
|
|
24
24
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
25
25
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
26
26
|
);
|
|
27
|
-
ALTER TABLE public.
|
|
28
|
-
CREATE INDEX IF NOT EXISTS
|
|
27
|
+
ALTER TABLE public.plg_inventory_stock_locations ENABLE ROW LEVEL SECURITY;
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_locations_tenant ON public.plg_inventory_stock_locations(tenant_id);
|
|
29
29
|
|
|
30
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
30
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_stock_movements (
|
|
31
31
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
32
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
33
|
-
product_id uuid NOT NULL REFERENCES
|
|
32
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
33
|
+
product_id uuid NOT NULL REFERENCES public.products(id),
|
|
34
34
|
quantity numeric(14,4) NOT NULL,
|
|
35
35
|
movement_type text NOT NULL,
|
|
36
36
|
unit_cost numeric(14,2) DEFAULT 0,
|
|
37
37
|
total_cost numeric(14,2) DEFAULT 0,
|
|
38
|
-
stock_location_id uuid REFERENCES public.
|
|
39
|
-
destination_location_id uuid REFERENCES public.
|
|
40
|
-
supplier_id uuid REFERENCES
|
|
38
|
+
stock_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),
|
|
39
|
+
destination_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),
|
|
40
|
+
supplier_id uuid REFERENCES public.people(id),
|
|
41
41
|
document_number text,
|
|
42
42
|
reason text,
|
|
43
43
|
notes text,
|
|
@@ -48,22 +48,22 @@ CREATE TABLE IF NOT EXISTS public.stock_movements (
|
|
|
48
48
|
metadata jsonb DEFAULT '{}'::jsonb,
|
|
49
49
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
50
50
|
);
|
|
51
|
-
ALTER TABLE public.
|
|
52
|
-
CREATE INDEX IF NOT EXISTS
|
|
53
|
-
CREATE INDEX IF NOT EXISTS
|
|
54
|
-
CREATE INDEX IF NOT EXISTS
|
|
51
|
+
ALTER TABLE public.plg_inventory_stock_movements ENABLE ROW LEVEL SECURITY;
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_tenant ON public.plg_inventory_stock_movements(tenant_id);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_product ON public.plg_inventory_stock_movements(product_id);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_date ON public.plg_inventory_stock_movements(tenant_id, movement_date);
|
|
55
55
|
|
|
56
|
-
CREATE TABLE IF NOT EXISTS public.
|
|
56
|
+
CREATE TABLE IF NOT EXISTS public.plg_inventory_stock_positions (
|
|
57
57
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
58
|
-
tenant_id uuid NOT NULL REFERENCES
|
|
59
|
-
product_id uuid NOT NULL REFERENCES
|
|
58
|
+
tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
|
59
|
+
product_id uuid NOT NULL REFERENCES public.products(id),
|
|
60
60
|
quantity numeric(14,4) NOT NULL,
|
|
61
61
|
unit_cost numeric(14,2) DEFAULT 0,
|
|
62
|
-
stock_location_id uuid REFERENCES public.
|
|
62
|
+
stock_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),
|
|
63
63
|
batch_number text,
|
|
64
64
|
expiration_date date,
|
|
65
65
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
66
66
|
);
|
|
67
|
-
ALTER TABLE public.
|
|
68
|
-
CREATE INDEX IF NOT EXISTS
|
|
69
|
-
CREATE INDEX IF NOT EXISTS
|
|
67
|
+
ALTER TABLE public.plg_inventory_stock_positions ENABLE ROW LEVEL SECURITY;
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_positions_tenant ON public.plg_inventory_stock_positions(tenant_id);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_positions_product ON public.plg_inventory_stock_positions(product_id);
|