@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
@@ -1,11 +1,11 @@
1
1
  -- Inventory Plugin: Recipes & Technical Specs
2
2
 
3
- CREATE TABLE IF NOT EXISTS public.recipes (
3
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_recipes (
4
4
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
5
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
5
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
6
6
  name text NOT NULL,
7
7
  description text,
8
- product_id uuid REFERENCES saas_core.products(id),
8
+ product_id uuid REFERENCES public.products(id),
9
9
  yield_quantity numeric(14,4) DEFAULT 1,
10
10
  yield_unit_id uuid,
11
11
  preparation_time_minutes integer,
@@ -14,21 +14,21 @@ CREATE TABLE IF NOT EXISTS public.recipes (
14
14
  created_at timestamptz NOT NULL DEFAULT now(),
15
15
  updated_at timestamptz NOT NULL DEFAULT now()
16
16
  );
17
- ALTER TABLE public.recipes ENABLE ROW LEVEL SECURITY;
18
- CREATE INDEX IF NOT EXISTS idx_recipes_tenant ON public.recipes(tenant_id);
19
- CREATE INDEX IF NOT EXISTS idx_recipes_product ON public.recipes(product_id);
17
+ ALTER TABLE public.plg_inventory_recipes ENABLE ROW LEVEL SECURITY;
18
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipes_tenant ON public.plg_inventory_recipes(tenant_id);
19
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipes_product ON public.plg_inventory_recipes(product_id);
20
20
 
21
- CREATE TABLE IF NOT EXISTS public.recipe_ingredients (
21
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_recipe_ingredients (
22
22
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
23
- recipe_id uuid NOT NULL REFERENCES public.recipes(id) ON DELETE CASCADE,
24
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
25
- product_id uuid NOT NULL REFERENCES saas_core.products(id),
23
+ recipe_id uuid NOT NULL REFERENCES public.plg_inventory_recipes(id) ON DELETE CASCADE,
24
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
25
+ product_id uuid NOT NULL REFERENCES public.products(id),
26
26
  quantity numeric(14,4) NOT NULL,
27
27
  unit_id uuid,
28
28
  display_order integer DEFAULT 0,
29
29
  notes text,
30
30
  created_at timestamptz NOT NULL DEFAULT now()
31
31
  );
32
- ALTER TABLE public.recipe_ingredients ENABLE ROW LEVEL SECURITY;
33
- CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_tenant ON public.recipe_ingredients(tenant_id);
34
- CREATE INDEX IF NOT EXISTS idx_recipe_ingredients_recipe ON public.recipe_ingredients(recipe_id);
32
+ ALTER TABLE public.plg_inventory_recipe_ingredients ENABLE ROW LEVEL SECURITY;
33
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipe_ingredients_tenant ON public.plg_inventory_recipe_ingredients(tenant_id);
34
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipe_ingredients_recipe ON public.plg_inventory_recipe_ingredients(recipe_id);
@@ -1,13 +1,13 @@
1
1
  -- Inventory Plugin: Measurement Units
2
2
 
3
- CREATE TABLE IF NOT EXISTS public.measurement_units (
3
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_measurement_units (
4
4
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
5
- tenant_id uuid NOT NULL REFERENCES saas_core.tenants(id) ON DELETE CASCADE,
5
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
6
6
  name text NOT NULL,
7
7
  abbreviation text NOT NULL,
8
8
  is_active boolean NOT NULL DEFAULT true,
9
9
  created_at timestamptz NOT NULL DEFAULT now(),
10
10
  updated_at timestamptz NOT NULL DEFAULT now()
11
11
  );
12
- ALTER TABLE public.measurement_units ENABLE ROW LEVEL SECURITY;
13
- CREATE INDEX IF NOT EXISTS idx_measurement_units_tenant ON public.measurement_units(tenant_id);
12
+ ALTER TABLE public.plg_inventory_measurement_units ENABLE ROW LEVEL SECURITY;
13
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_measurement_units_tenant ON public.plg_inventory_measurement_units(tenant_id);
@@ -0,0 +1,161 @@
1
+ // AUTO-GENERATED from 000_plg_rename.sql, 001_inventory_base.sql, 002_recipes.sql, 003_measurement_units.sql — regenerate with scripts/embed-migrations.mjs
2
+ // SQL files are the source of truth; this inline copy lets the manifest declare
3
+ // migrations as data. Do not edit by hand — run the embed script instead.
4
+
5
+ export const MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql — rename legacy inventory tables to plg_inventory_* for pools
6
+ -- provisioned before the industry-pool rename. Guarded: fires only when the legacy
7
+ -- name exists and the target does not, so fresh pools skip every branch.
8
+ DO $$
9
+ BEGIN
10
+ IF to_regclass('public.stock_locations') IS NOT NULL AND to_regclass('public.plg_inventory_stock_locations') IS NULL THEN
11
+ ALTER TABLE public.stock_locations RENAME TO plg_inventory_stock_locations;
12
+ END IF;
13
+ IF to_regclass('public.stock_movements') IS NOT NULL AND to_regclass('public.plg_inventory_stock_movements') IS NULL THEN
14
+ ALTER TABLE public.stock_movements RENAME TO plg_inventory_stock_movements;
15
+ END IF;
16
+ IF to_regclass('public.stock_positions') IS NOT NULL AND to_regclass('public.plg_inventory_stock_positions') IS NULL THEN
17
+ ALTER TABLE public.stock_positions RENAME TO plg_inventory_stock_positions;
18
+ END IF;
19
+ IF to_regclass('public.recipes') IS NOT NULL AND to_regclass('public.plg_inventory_recipes') IS NULL THEN
20
+ ALTER TABLE public.recipes RENAME TO plg_inventory_recipes;
21
+ END IF;
22
+ IF to_regclass('public.recipe_ingredients') IS NOT NULL AND to_regclass('public.plg_inventory_recipe_ingredients') IS NULL THEN
23
+ ALTER TABLE public.recipe_ingredients RENAME TO plg_inventory_recipe_ingredients;
24
+ END IF;
25
+ IF to_regclass('public.measurement_units') IS NOT NULL AND to_regclass('public.plg_inventory_measurement_units') IS NULL THEN
26
+ ALTER TABLE public.measurement_units RENAME TO plg_inventory_measurement_units;
27
+ END IF;
28
+ IF to_regclass('public.product_categories') IS NOT NULL AND to_regclass('public.plg_inventory_product_categories') IS NULL THEN
29
+ ALTER TABLE public.product_categories RENAME TO plg_inventory_product_categories;
30
+ END IF;
31
+ END $$;
32
+ `
33
+
34
+ export const MIGRATION_001_INVENTORY_BASE = `-- Inventory Plugin: Base Tables
35
+ -- Products use public.products archetype directly
36
+ -- These are plugin-specific extension tables
37
+
38
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_product_categories (
39
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
40
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
41
+ name text NOT NULL,
42
+ parent_id uuid REFERENCES public.plg_inventory_product_categories(id),
43
+ is_active boolean NOT NULL DEFAULT true,
44
+ created_at timestamptz NOT NULL DEFAULT now(),
45
+ updated_at timestamptz NOT NULL DEFAULT now()
46
+ );
47
+ ALTER TABLE public.plg_inventory_product_categories ENABLE ROW LEVEL SECURITY;
48
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_product_categories_tenant ON public.plg_inventory_product_categories(tenant_id);
49
+
50
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_stock_locations (
51
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
52
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
53
+ name text NOT NULL,
54
+ description text,
55
+ is_active boolean NOT NULL DEFAULT true,
56
+ unit_id uuid,
57
+ created_at timestamptz NOT NULL DEFAULT now(),
58
+ updated_at timestamptz NOT NULL DEFAULT now()
59
+ );
60
+ ALTER TABLE public.plg_inventory_stock_locations ENABLE ROW LEVEL SECURITY;
61
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_locations_tenant ON public.plg_inventory_stock_locations(tenant_id);
62
+
63
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_stock_movements (
64
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
65
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
66
+ product_id uuid NOT NULL REFERENCES public.products(id),
67
+ quantity numeric(14,4) NOT NULL,
68
+ movement_type text NOT NULL,
69
+ unit_cost numeric(14,2) DEFAULT 0,
70
+ total_cost numeric(14,2) DEFAULT 0,
71
+ stock_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),
72
+ destination_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),
73
+ supplier_id uuid REFERENCES public.people(id),
74
+ document_number text,
75
+ reason text,
76
+ notes text,
77
+ movement_date date NOT NULL DEFAULT CURRENT_DATE,
78
+ user_id uuid,
79
+ batch_number text,
80
+ expiration_date date,
81
+ metadata jsonb DEFAULT '{}'::jsonb,
82
+ created_at timestamptz NOT NULL DEFAULT now()
83
+ );
84
+ ALTER TABLE public.plg_inventory_stock_movements ENABLE ROW LEVEL SECURITY;
85
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_tenant ON public.plg_inventory_stock_movements(tenant_id);
86
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_product ON public.plg_inventory_stock_movements(product_id);
87
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_movements_date ON public.plg_inventory_stock_movements(tenant_id, movement_date);
88
+
89
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_stock_positions (
90
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
91
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
92
+ product_id uuid NOT NULL REFERENCES public.products(id),
93
+ quantity numeric(14,4) NOT NULL,
94
+ unit_cost numeric(14,2) DEFAULT 0,
95
+ stock_location_id uuid REFERENCES public.plg_inventory_stock_locations(id),
96
+ batch_number text,
97
+ expiration_date date,
98
+ created_at timestamptz NOT NULL DEFAULT now()
99
+ );
100
+ ALTER TABLE public.plg_inventory_stock_positions ENABLE ROW LEVEL SECURITY;
101
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_positions_tenant ON public.plg_inventory_stock_positions(tenant_id);
102
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_stock_positions_product ON public.plg_inventory_stock_positions(product_id);
103
+ `
104
+
105
+ export const MIGRATION_002_RECIPES = `-- Inventory Plugin: Recipes & Technical Specs
106
+
107
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_recipes (
108
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
109
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
110
+ name text NOT NULL,
111
+ description text,
112
+ product_id uuid REFERENCES public.products(id),
113
+ yield_quantity numeric(14,4) DEFAULT 1,
114
+ yield_unit_id uuid,
115
+ preparation_time_minutes integer,
116
+ instructions text,
117
+ is_active boolean NOT NULL DEFAULT true,
118
+ created_at timestamptz NOT NULL DEFAULT now(),
119
+ updated_at timestamptz NOT NULL DEFAULT now()
120
+ );
121
+ ALTER TABLE public.plg_inventory_recipes ENABLE ROW LEVEL SECURITY;
122
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipes_tenant ON public.plg_inventory_recipes(tenant_id);
123
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipes_product ON public.plg_inventory_recipes(product_id);
124
+
125
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_recipe_ingredients (
126
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
127
+ recipe_id uuid NOT NULL REFERENCES public.plg_inventory_recipes(id) ON DELETE CASCADE,
128
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
129
+ product_id uuid NOT NULL REFERENCES public.products(id),
130
+ quantity numeric(14,4) NOT NULL,
131
+ unit_id uuid,
132
+ display_order integer DEFAULT 0,
133
+ notes text,
134
+ created_at timestamptz NOT NULL DEFAULT now()
135
+ );
136
+ ALTER TABLE public.plg_inventory_recipe_ingredients ENABLE ROW LEVEL SECURITY;
137
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipe_ingredients_tenant ON public.plg_inventory_recipe_ingredients(tenant_id);
138
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_recipe_ingredients_recipe ON public.plg_inventory_recipe_ingredients(recipe_id);
139
+ `
140
+
141
+ export const MIGRATION_003_MEASUREMENT_UNITS = `-- Inventory Plugin: Measurement Units
142
+
143
+ CREATE TABLE IF NOT EXISTS public.plg_inventory_measurement_units (
144
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
145
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
146
+ name text NOT NULL,
147
+ abbreviation text NOT NULL,
148
+ is_active boolean NOT NULL DEFAULT true,
149
+ created_at timestamptz NOT NULL DEFAULT now(),
150
+ updated_at timestamptz NOT NULL DEFAULT now()
151
+ );
152
+ ALTER TABLE public.plg_inventory_measurement_units ENABLE ROW LEVEL SECURITY;
153
+ CREATE INDEX IF NOT EXISTS idx_plg_inventory_measurement_units_tenant ON public.plg_inventory_measurement_units(tenant_id);
154
+ `
155
+
156
+ export const MIGRATIONS: Array<{ id: string; sql: string }> = [
157
+ { id: "000_plg_rename", sql: MIGRATION_000_PLG_RENAME },
158
+ { id: "001_inventory_base", sql: MIGRATION_001_INVENTORY_BASE },
159
+ { id: "002_recipes", sql: MIGRATION_002_RECIPES },
160
+ { id: "003_measurement_units", sql: MIGRATION_003_MEASUREMENT_UNITS },
161
+ ]
package/src/registries.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { PluginRegistryDef } from '@fayz-ai/core'
2
2
  import type { EntityDef } from '@fayz-ai/core'
3
+ import { T } from './data/tables'
3
4
 
4
5
  const supplierEntity: EntityDef = {
5
6
  name: 'Supplier',
@@ -18,8 +19,7 @@ const supplierEntity: EntityDef = {
18
19
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
19
20
  ],
20
21
  data: {
21
- table: 'persons',
22
- schema: 'saas_core',
22
+ table: 'people',
23
23
  tenantScoped: true,
24
24
  filters: { kind: 'supplier' },
25
25
  defaults: { kind: 'supplier' },
@@ -37,7 +37,7 @@ const measurementUnitEntity: EntityDef = {
37
37
  { key: 'abbreviation', label: 'Abbreviation', type: 'text', required: true, showInTable: true },
38
38
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
39
39
  ],
40
- data: { table: 'measurement_units', tenantScoped: true },
40
+ data: { table: T.measurementUnits, tenantScoped: true },
41
41
  }
42
42
 
43
43
  const productCategoryEntity: EntityDef = {
@@ -51,7 +51,7 @@ const productCategoryEntity: EntityDef = {
51
51
  { key: 'parentId', label: 'Parent', type: 'text', showInTable: false },
52
52
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
53
53
  ],
54
- data: { table: 'product_categories', tenantScoped: true },
54
+ data: { table: T.productCategories, tenantScoped: true },
55
55
  }
56
56
 
57
57
  const stockLocationEntity: EntityDef = {
@@ -65,7 +65,7 @@ const stockLocationEntity: EntityDef = {
65
65
  { key: 'description', label: 'Description', type: 'textarea', showInTable: true },
66
66
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
67
67
  ],
68
- data: { table: 'stock_locations', tenantScoped: true },
68
+ data: { table: T.stockLocations, tenantScoped: true },
69
69
  }
70
70
 
71
71
  export const inventoryRegistries: PluginRegistryDef[] = [
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { SubpageHeader, toast } from '@fayz-ai/ui'
3
3
  import { useTranslation } from '@fayz-ai/core'
4
- import { CrudFormPage } from '@fayz-ai/saas'
4
+ import { CrudFormPage, useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
5
5
  import { useInventoryConfig, useInventoryStore, useInventoryProvider } from '../InventoryContext'
6
6
  import type { ProductType } from '../types'
7
7
  import { buildProductEntity } from './productEntity'
@@ -19,6 +19,7 @@ export function ProductCrudForm({ editId, onSaved }: { editId?: string; onSaved?
19
19
  const { productTypes, currency } = useInventoryConfig()
20
20
  const provider = useInventoryProvider()
21
21
  const createProduct = useInventoryStore((s) => s.createProduct)
22
+ const guardProducts = useLimitGuard('products')
22
23
  const isEdit = !!editId
23
24
 
24
25
  const [initialData, setInitialData] = React.useState<Record<string, any> | null>(isEdit ? null : {})
@@ -72,7 +73,11 @@ export function ProductCrudForm({ editId, onSaved }: { editId?: string; onSaved?
72
73
  if (isEdit && editId) {
73
74
  await provider.updateProduct(editId, payload)
74
75
  } else {
76
+ // Plan quantity guard (client-side, before the provider call). Opens the
77
+ // global UpgradeModal and aborts when the plan's product cap is reached.
78
+ if ((await guardProducts()) === 'blocked') return
75
79
  await createProduct(payload)
80
+ invalidateLimit('products')
76
81
  }
77
82
  onSaved?.()
78
83
  }
@@ -52,6 +52,7 @@ export function ProductListView({ onNew, onEdit }: {
52
52
  onNew={onNew}
53
53
  addLabel={t('inventory.productList.newProduct')}
54
54
  onRowClick={(row) => onEdit?.((row as { id: string }).id)}
55
+ feature="inventory"
55
56
  />
56
57
  )
57
58
  }
@@ -5,6 +5,7 @@ import { toast } from '@fayz-ai/ui'
5
5
  import { SubpageHeader, useSaveBar } from '@fayz-ai/ui'
6
6
  import { useTranslation } from '@fayz-ai/core'
7
7
  import { SearchSelect } from '@fayz-ai/ui'
8
+ import { useLimitGuard, invalidateLimit } from '@fayz-ai/saas'
8
9
  import type { CreateRecipeIngredientInput } from '../types'
9
10
 
10
11
  interface FormIngredient {
@@ -24,6 +25,7 @@ export function RecipeFormView({ onSaved }: { onSaved?: (id?: string) => void })
24
25
  const t = useTranslation()
25
26
  const provider = useInventoryProvider()
26
27
  const createRecipe = useInventoryStore((s) => s.createRecipe)
28
+ const guardRecipes = useLimitGuard('recipes')
27
29
 
28
30
  const [name, setName] = useState('')
29
31
  const [description, setDescription] = useState('')
@@ -49,6 +51,9 @@ export function RecipeFormView({ onSaved }: { onSaved?: (id?: string) => void })
49
51
 
50
52
  async function handleSave() {
51
53
  if (!name.trim() || !productId || ingredients.length === 0) { toast.error(t('common.formIncomplete')); return }
54
+ // Plan quantity guard (client-side, before the provider call). Opens the
55
+ // global UpgradeModal and aborts when the plan's recipe cap is reached.
56
+ if ((await guardRecipes()) === 'blocked') return
52
57
  setSaving(true)
53
58
  try {
54
59
  const recipe = await createRecipe({
@@ -66,6 +71,7 @@ export function RecipeFormView({ onSaved }: { onSaved?: (id?: string) => void })
66
71
  notes: i.notes || undefined,
67
72
  })),
68
73
  })
74
+ invalidateLimit('recipes')
69
75
  onSaved?.(recipe.id)
70
76
  } finally { setSaving(false) }
71
77
  }
@@ -3,6 +3,7 @@ import { BookOpen, Plus, Clock, Layers } from 'lucide-react'
3
3
  import { useInventoryStore } from '../InventoryContext'
4
4
  import { useTranslation } from '@fayz-ai/core'
5
5
  import { SubpageHeader } from '@fayz-ai/ui'
6
+ import { PermissionGate } from '@fayz-ai/saas'
6
7
 
7
8
  function RecipeSkeleton() {
8
9
  return (
@@ -40,9 +41,11 @@ export function RecipesView({ onNew, onView }: { onNew?: () => void; onView?: (i
40
41
  title={t('inventory.recipes.title')}
41
42
  subtitle={t('inventory.recipes.productionFormulas', { count: String(recipes.length) })}
42
43
  actions={onNew && (
43
- <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">
44
- <Plus className="h-3.5 w-3.5" /> {t('inventory.recipes.newRecipe')}
45
- </button>
44
+ <PermissionGate feature="inventory" action="create">
45
+ <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">
46
+ <Plus className="h-3.5 w-3.5" /> {t('inventory.recipes.newRecipe')}
47
+ </button>
48
+ </PermissionGate>
46
49
  )}
47
50
  />
48
51