@fayz-ai/plugin-inventory 0.8.0 → 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/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/index.cjs +75 -24
- 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 +76 -25
- package/dist/index.js.map +1 -1
- 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 +4 -4
- package/src/InventoryContext.tsx +2 -0
- package/src/InventoryPage.tsx +13 -7
- package/src/components/InventoryGeneralSettings.tsx +21 -8
- package/src/index.ts +43 -0
- 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
|
@@ -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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "beta"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.8.
|
|
6
|
+
"version": "0.8.1",
|
|
7
7
|
"description": "Fayz SDK — plugin-inventory plugin",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./dist/index.cjs",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"lucide-react": "^0.400.0",
|
|
30
30
|
"zustand": "^4.5.0",
|
|
31
31
|
"@tanstack/react-table": "^8.20.0",
|
|
32
|
-
"@fayz-ai/
|
|
33
|
-
"@fayz-ai/
|
|
34
|
-
"@fayz-ai/core": "^0.8.
|
|
32
|
+
"@fayz-ai/saas": "^0.8.2",
|
|
33
|
+
"@fayz-ai/ui": "^0.8.1",
|
|
34
|
+
"@fayz-ai/core": "^0.8.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/react": "^18.3.0",
|
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}`)} /> },
|
|
@@ -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/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',
|
|
@@ -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
|
}
|
|
@@ -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
|
-
<
|
|
44
|
-
<
|
|
45
|
-
|
|
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
|
|