@fayz-ai/plugin-inventory 0.1.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 +37 -0
- package/dist/InventoryContext.d.ts.map +1 -0
- package/dist/InventoryPage.d.ts +13 -0
- package/dist/InventoryPage.d.ts.map +1 -0
- package/dist/components/InventoryGeneralSettings.d.ts +3 -0
- package/dist/components/InventoryGeneralSettings.d.ts.map +1 -0
- package/dist/components/InventoryOnboarding.d.ts +5 -0
- package/dist/components/InventoryOnboarding.d.ts.map +1 -0
- package/dist/components/InventorySettings.d.ts +8 -0
- package/dist/components/InventorySettings.d.ts.map +1 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +22 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +2936 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2930 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +28 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/DashboardView.d.ts +8 -0
- package/dist/views/DashboardView.d.ts.map +1 -0
- package/dist/views/MovementHistoryView.d.ts +5 -0
- package/dist/views/MovementHistoryView.d.ts.map +1 -0
- package/dist/views/ProductCrudForm.d.ts +6 -0
- package/dist/views/ProductCrudForm.d.ts.map +1 -0
- package/dist/views/ProductFormView.d.ts +6 -0
- package/dist/views/ProductFormView.d.ts.map +1 -0
- package/dist/views/ProductListView.d.ts +6 -0
- package/dist/views/ProductListView.d.ts.map +1 -0
- package/dist/views/RecipeDetailView.d.ts +6 -0
- package/dist/views/RecipeDetailView.d.ts.map +1 -0
- package/dist/views/RecipeFormView.d.ts +5 -0
- package/dist/views/RecipeFormView.d.ts.map +1 -0
- package/dist/views/RecipesView.d.ts +6 -0
- package/dist/views/RecipesView.d.ts.map +1 -0
- package/dist/views/StockMovementView.d.ts +8 -0
- package/dist/views/StockMovementView.d.ts.map +1 -0
- package/dist/views/dashboardWidgets.d.ts +11 -0
- package/dist/views/dashboardWidgets.d.ts.map +1 -0
- package/dist/views/productEntity.d.ts +6 -0
- package/dist/views/productEntity.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/InventoryContext.tsx +40 -0
- package/src/InventoryPage.tsx +170 -0
- package/src/README.md +177 -0
- package/src/components/InventoryGeneralSettings.tsx +26 -0
- package/src/components/InventoryOnboarding.tsx +60 -0
- package/src/components/InventorySettings.tsx +27 -0
- package/src/data/index.ts +2 -0
- package/src/data/mock.ts +266 -0
- package/src/data/supabase.ts +358 -0
- package/src/data/types.ts +35 -0
- package/src/index.ts +191 -0
- package/src/lib/tenant.ts +4 -0
- package/src/locales/en.ts +242 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +242 -0
- package/src/migrations/001_inventory_base.sql +69 -0
- package/src/migrations/002_recipes.sql +34 -0
- package/src/migrations/003_measurement_units.sql +13 -0
- package/src/registries.ts +111 -0
- package/src/store.ts +127 -0
- package/src/types.ts +256 -0
- package/src/views/DashboardView.tsx +11 -0
- package/src/views/MovementHistoryView.tsx +104 -0
- package/src/views/ProductCrudForm.tsx +99 -0
- package/src/views/ProductFormView.tsx +283 -0
- package/src/views/ProductListView.tsx +107 -0
- package/src/views/RecipeDetailView.tsx +192 -0
- package/src/views/RecipeFormView.tsx +235 -0
- package/src/views/RecipesView.tsx +103 -0
- package/src/views/StockMovementView.tsx +516 -0
- package/src/views/dashboardWidgets.tsx +101 -0
- package/src/views/productEntity.tsx +124 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react'
|
|
2
|
+
import type { StoreApi } from 'zustand/vanilla'
|
|
3
|
+
import { ModulePage, PageTransition, type ModuleNavItem } from '@fayz-ai/ui'
|
|
4
|
+
import { InventoryContextProvider, type ResolvedInventoryConfig } from './InventoryContext'
|
|
5
|
+
import type { InventoryDataProvider } from './data/types'
|
|
6
|
+
import type { InventoryUIState } from './store'
|
|
7
|
+
import type { PluginRegistryDef, PluginQuickAction } from '@fayz-ai/core'
|
|
8
|
+
import { useModuleNavigation, ModuleActionBar, createViewRouter } from '@fayz-ai/saas'
|
|
9
|
+
import { useTranslation } from '@fayz-ai/core'
|
|
10
|
+
import { DashboardView } from './views/DashboardView'
|
|
11
|
+
import { ProductListView } from './views/ProductListView'
|
|
12
|
+
import { ProductFormView } from './views/ProductFormView'
|
|
13
|
+
import { StockMovementView } from './views/StockMovementView'
|
|
14
|
+
import { MovementHistoryView } from './views/MovementHistoryView'
|
|
15
|
+
import { RecipesView } from './views/RecipesView'
|
|
16
|
+
import { RecipeFormView } from './views/RecipeFormView'
|
|
17
|
+
import { RecipeDetailView } from './views/RecipeDetailView'
|
|
18
|
+
import { InventoryGeneralSettings } from './components/InventoryGeneralSettings'
|
|
19
|
+
import { InventoryOnboarding } from './components/InventoryOnboarding'
|
|
20
|
+
|
|
21
|
+
function buildNav(config: ResolvedInventoryConfig, view: string, navigate: (v: string) => void, t: (key: string) => string): ModuleNavItem[] {
|
|
22
|
+
const items: ModuleNavItem[] = [
|
|
23
|
+
{ id: 'dashboard', label: t('inventory.nav.dashboard'), icon: 'BarChart3', active: view === 'dashboard', onClick: () => navigate('dashboard') },
|
|
24
|
+
{
|
|
25
|
+
id: 'products', label: t('inventory.nav.products'), icon: 'Package', active: view.startsWith('products'),
|
|
26
|
+
children: [
|
|
27
|
+
{ id: 'products-new', label: t('inventory.nav.new'), active: view === 'products-new', onClick: () => navigate('products-new') },
|
|
28
|
+
{ id: 'products-list', label: t('inventory.nav.list'), active: view === 'products-list', onClick: () => navigate('products-list') },
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'stock', label: t('inventory.nav.stock'), icon: 'ArrowUpCircle',
|
|
33
|
+
children: [
|
|
34
|
+
{ id: 'stock-entry', label: t('inventory.nav.entry'), active: view === 'stock-entry', onClick: () => navigate('stock-entry') },
|
|
35
|
+
{ id: 'stock-exit', label: t('inventory.nav.exit'), active: view === 'stock-exit', onClick: () => navigate('stock-exit') },
|
|
36
|
+
{ id: 'stock-history', label: t('inventory.nav.history'), active: view === 'stock-history', onClick: () => navigate('stock-history') },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
if (config.modules.recipes) {
|
|
42
|
+
items.push({
|
|
43
|
+
id: 'recipes', label: config.labels.recipes, icon: 'BookOpen',
|
|
44
|
+
children: [
|
|
45
|
+
{ id: 'recipes-list', label: config.labels.recipesList, active: view === 'recipes-list' || view.startsWith('recipes-detail:'), onClick: () => navigate('recipes-list') },
|
|
46
|
+
{ id: 'recipes-new', label: config.labels.recipesNew, active: view === 'recipes-new', onClick: () => navigate('recipes-new') },
|
|
47
|
+
],
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return items
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function InventoryPage({ config, provider, store, registries }: {
|
|
55
|
+
config: ResolvedInventoryConfig
|
|
56
|
+
provider: InventoryDataProvider
|
|
57
|
+
store: StoreApi<InventoryUIState>
|
|
58
|
+
registries?: PluginRegistryDef[]
|
|
59
|
+
}) {
|
|
60
|
+
const t = useTranslation()
|
|
61
|
+
const { view, direction, navigate } = useModuleNavigation('/inventory', {
|
|
62
|
+
dashboard: 0,
|
|
63
|
+
'products-list': 0, 'products-new': 1,
|
|
64
|
+
'stock-entry': 1, 'stock-exit': 1, 'stock-history': 0,
|
|
65
|
+
'recipes-list': 0, 'recipes-new': 1, 'recipes-detail': 1,
|
|
66
|
+
settings: 1,
|
|
67
|
+
}, 'dashboard')
|
|
68
|
+
|
|
69
|
+
const [onboardingComplete, setOnboardingComplete] = useState(() => {
|
|
70
|
+
try { return localStorage.getItem('saas-core:inventory-onboarded') === 'true' } catch { return false }
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const isSettings = view === 'settings'
|
|
74
|
+
const isSummary = view === 'dashboard' || view === ''
|
|
75
|
+
const nav = buildNav(config, view, navigate, t)
|
|
76
|
+
|
|
77
|
+
const quickActions = useMemo<PluginQuickAction[]>(() => {
|
|
78
|
+
const actions: PluginQuickAction[] = [
|
|
79
|
+
{
|
|
80
|
+
id: 'new-product',
|
|
81
|
+
label: t('inventory.quickActions.newProduct'),
|
|
82
|
+
icon: 'Package',
|
|
83
|
+
description: t('inventory.quickActions.newProductDesc'),
|
|
84
|
+
action: () => navigate('products-new'),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'stock-entry',
|
|
88
|
+
label: t('inventory.quickActions.stockEntry'),
|
|
89
|
+
icon: 'ArrowUpRight',
|
|
90
|
+
description: t('inventory.quickActions.stockEntryDesc'),
|
|
91
|
+
action: () => navigate('stock-entry'),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'stock-exit',
|
|
95
|
+
label: t('inventory.quickActions.stockExit'),
|
|
96
|
+
icon: 'ArrowDownRight',
|
|
97
|
+
description: t('inventory.quickActions.stockExitDesc'),
|
|
98
|
+
action: () => navigate('stock-exit'),
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
return actions
|
|
102
|
+
}, [])
|
|
103
|
+
|
|
104
|
+
if (!onboardingComplete) {
|
|
105
|
+
return (
|
|
106
|
+
<InventoryContextProvider config={config} provider={provider} store={store}>
|
|
107
|
+
<InventoryOnboarding onComplete={() => { setOnboardingComplete(true); try { localStorage.setItem('saas-core:inventory-onboarded', 'true') } catch {} }} />
|
|
108
|
+
</InventoryContextProvider>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isSettings && registries && registries.length > 0) {
|
|
113
|
+
return (
|
|
114
|
+
<InventoryContextProvider config={config} provider={provider} store={store}>
|
|
115
|
+
<PageTransition transitionKey="settings" direction={direction}>
|
|
116
|
+
<div style={{ padding: '24px' }}>
|
|
117
|
+
<div style={{ marginBottom: '16px' }}>
|
|
118
|
+
<h1 style={{ fontSize: '20px', fontWeight: 600, margin: 0 }}>{t('inventory.settingsPage.title')}</h1>
|
|
119
|
+
<p style={{ color: 'var(--muted-foreground, #6b7280)', margin: '4px 0 0', fontSize: '14px' }}>
|
|
120
|
+
{t('inventory.settingsPage.subtitle')}
|
|
121
|
+
</p>
|
|
122
|
+
</div>
|
|
123
|
+
<InventoryGeneralSettings />
|
|
124
|
+
</div>
|
|
125
|
+
</PageTransition>
|
|
126
|
+
</InventoryContextProvider>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const renderView = createViewRouter([
|
|
131
|
+
{ id: 'products-list', render: () => <ProductListView onNew={() => navigate('products-new')} onEdit={(id) => navigate(`products-edit:${id}`)} /> },
|
|
132
|
+
{ id: 'products-new', render: () => <ProductFormView onSaved={() => navigate('products-list')} /> },
|
|
133
|
+
{ id: 'products-edit', render: ({ id }) => <ProductFormView editId={id!} onSaved={() => navigate('products-list')} /> },
|
|
134
|
+
{ id: 'stock-entry', render: () => <StockMovementView defaultType="entry" onSaved={() => navigate('stock-history')} /> },
|
|
135
|
+
{ id: 'stock-exit', render: () => <StockMovementView defaultType="exit" onSaved={() => navigate('stock-history')} /> },
|
|
136
|
+
{ id: 'stock-history', render: () => <MovementHistoryView onViewDetail={(id) => navigate(`stock-detail:${id}`)} /> },
|
|
137
|
+
{ id: 'stock-detail', render: ({ id }) => {
|
|
138
|
+
const movement = store.getState().movements.find((m) => m.id === id)
|
|
139
|
+
return movement
|
|
140
|
+
? <StockMovementView defaultType={movement.movementType} viewMovement={movement} onSaved={() => navigate('stock-history')} />
|
|
141
|
+
: <MovementHistoryView onViewDetail={(mid) => navigate(`stock-detail:${mid}`)} />
|
|
142
|
+
} },
|
|
143
|
+
{ id: 'recipes-list', render: () => <RecipesView onNew={() => navigate('recipes-new')} onView={(id) => navigate(`recipes-detail:${id}`)} /> },
|
|
144
|
+
{ id: 'recipes-new', render: () => <RecipeFormView onSaved={(id) => id ? navigate(`recipes-detail:${id}`) : navigate('recipes-list')} /> },
|
|
145
|
+
{ id: 'recipes-detail', render: ({ id }) => <RecipeDetailView recipeId={id!} onBack={() => navigate('recipes-list')} /> },
|
|
146
|
+
{ id: 'dashboard', render: () => <DashboardView /> },
|
|
147
|
+
], 'dashboard')
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<InventoryContextProvider config={config} provider={provider} store={store}>
|
|
151
|
+
<ModulePage
|
|
152
|
+
title={config.labels.pageTitle}
|
|
153
|
+
subtitle={config.labels.pageSubtitle}
|
|
154
|
+
nav={nav}
|
|
155
|
+
showHeader={isSummary}
|
|
156
|
+
viewKey={view}
|
|
157
|
+
direction={direction}
|
|
158
|
+
headerAction={
|
|
159
|
+
<ModuleActionBar
|
|
160
|
+
quickActions={quickActions}
|
|
161
|
+
settingsPath={registries && registries.length > 0 ? '/settings/inventory' : undefined}
|
|
162
|
+
settingsLabel="Inventory Settings"
|
|
163
|
+
/>
|
|
164
|
+
}
|
|
165
|
+
>
|
|
166
|
+
{renderView(view)}
|
|
167
|
+
</ModulePage>
|
|
168
|
+
</InventoryContextProvider>
|
|
169
|
+
)
|
|
170
|
+
}
|
package/src/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Inventory Plugin
|
|
2
|
+
|
|
3
|
+
Product catalog, stock management, movement tracking, and recipe/production formulas for any SaaS vertical.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { createInventoryPlugin } from '@fayz-ai/saas-core/plugins/inventory'
|
|
9
|
+
|
|
10
|
+
// In your createSaasApp config:
|
|
11
|
+
plugins: [
|
|
12
|
+
createInventoryPlugin({
|
|
13
|
+
currency: { code: 'BRL', locale: 'pt-BR', symbol: 'R$' },
|
|
14
|
+
}),
|
|
15
|
+
]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
| Option | Type | Default | Description |
|
|
21
|
+
|--------|------|---------|-------------|
|
|
22
|
+
| `modules.recipes` | boolean | true | Production recipes/technical specs |
|
|
23
|
+
| `modules.stockLocations` | boolean | true | Multi-location stock |
|
|
24
|
+
| `modules.batchTracking` | boolean | false | Batch numbers + expiry dates |
|
|
25
|
+
| `productTypes` | array | ingredient, sale, intermediate, asset | Available product classifications |
|
|
26
|
+
| `currency` | object | BRL/pt-BR | Currency code, locale, symbol |
|
|
27
|
+
| `locations` | array | [] | Business units (shows unit picker when 1+) |
|
|
28
|
+
| `dataProvider` | InventoryDataProvider | mock | Custom data provider |
|
|
29
|
+
| `navPosition` | number | 4 | Navigation order |
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
src/plugins/inventory/
|
|
35
|
+
index.ts # createInventoryPlugin() factory
|
|
36
|
+
types.ts # Pure TS domain types (zero deps)
|
|
37
|
+
registries.ts # CRUD entities: units, categories, locations
|
|
38
|
+
store.ts # Zustand UI state + provider actions
|
|
39
|
+
InventoryContext.tsx # React contexts: config, provider, store
|
|
40
|
+
InventoryPage.tsx # Main page with ModulePage layout
|
|
41
|
+
data/
|
|
42
|
+
types.ts # InventoryDataProvider interface
|
|
43
|
+
mock.ts # In-memory mock provider
|
|
44
|
+
views/ # Dashboard, Products, Stock, Recipes
|
|
45
|
+
components/ # Settings, onboarding
|
|
46
|
+
migrations/ # SQL migration files
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Data Flow
|
|
50
|
+
|
|
51
|
+
Same pattern as the Financial plugin:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
createInventoryPlugin(options)
|
|
55
|
+
→ resolves config
|
|
56
|
+
→ creates provider (mock or custom)
|
|
57
|
+
→ creates zustand store
|
|
58
|
+
→ returns PluginManifest
|
|
59
|
+
|
|
60
|
+
InventoryPage
|
|
61
|
+
→ InventoryContextProvider wraps all views
|
|
62
|
+
→ useModuleNavigation for view switching + animations
|
|
63
|
+
→ views call useInventoryStore(selector) for data
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Type System
|
|
67
|
+
|
|
68
|
+
### Product Types
|
|
69
|
+
|
|
70
|
+
Products are classified by type — a product can be one type at a time:
|
|
71
|
+
|
|
72
|
+
| Type | Description | Example |
|
|
73
|
+
|------|-------------|---------|
|
|
74
|
+
| `ingredient` | Raw material consumed in production | Hair dye, flour |
|
|
75
|
+
| `sale` | Sold directly to customers | Shampoo bottle, menu item |
|
|
76
|
+
| `intermediate` | Produced internally from ingredients | Pre-mixed color, dough |
|
|
77
|
+
| `asset` | Fixed asset for patrimony tracking | Equipment, furniture |
|
|
78
|
+
|
|
79
|
+
The `productTypes` option lets verticals customize available types:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Restaurant — simplified types
|
|
83
|
+
createInventoryPlugin({
|
|
84
|
+
productTypes: [
|
|
85
|
+
{ value: 'ingredient', label: 'Ingredient' },
|
|
86
|
+
{ value: 'sale', label: 'Menu Item' },
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Stock Movements
|
|
92
|
+
|
|
93
|
+
| Movement Type | Direction | Description |
|
|
94
|
+
|--------------|-----------|-------------|
|
|
95
|
+
| `entry` | +quantity | Receiving goods from supplier |
|
|
96
|
+
| `exit` | -quantity | Using/selling goods |
|
|
97
|
+
| `adjustment` | +/- | Quantity correction after audit |
|
|
98
|
+
| `transfer` | neutral | Between stock locations |
|
|
99
|
+
| `loss` | -quantity | Waste, damage, or expiry |
|
|
100
|
+
|
|
101
|
+
### Core Entities
|
|
102
|
+
|
|
103
|
+
| Entity | Purpose |
|
|
104
|
+
|--------|---------|
|
|
105
|
+
| `Product` | Master product with type, pricing, stock levels |
|
|
106
|
+
| `StockMovement` | Transaction log: entries, exits, adjustments |
|
|
107
|
+
| `StockPosition` | Physical inventory by location + batch + expiry |
|
|
108
|
+
| `StockLocation` | Warehouses, storage areas |
|
|
109
|
+
| `Recipe` | Production formula for intermediate/final products |
|
|
110
|
+
| `RecipeIngredient` | What goes into a recipe |
|
|
111
|
+
| `MeasurementUnit` | Units of measure (kg, L, unit, box) |
|
|
112
|
+
| `ProductCategory` | Hierarchical product organization |
|
|
113
|
+
|
|
114
|
+
## Registries (Plugin Settings)
|
|
115
|
+
|
|
116
|
+
Accessible via gear icon or global Settings > Inventory tab:
|
|
117
|
+
|
|
118
|
+
| Registry | Seed Data |
|
|
119
|
+
|----------|-----------|
|
|
120
|
+
| Measurement Units | Unit, Box, Kg, g, L, mL |
|
|
121
|
+
| Product Categories | — |
|
|
122
|
+
| Stock Locations | — |
|
|
123
|
+
|
|
124
|
+
## Database Migrations
|
|
125
|
+
|
|
126
|
+
Run in order:
|
|
127
|
+
|
|
128
|
+
1. `001_inventory_base.sql` — product_categories, stock_locations, products, stock_movements, stock_positions
|
|
129
|
+
2. `002_recipes.sql` — recipes, recipe_ingredients
|
|
130
|
+
3. `003_measurement_units.sql` — measurement_units
|
|
131
|
+
|
|
132
|
+
All tables use `tenant_id` for multi-tenant isolation.
|
|
133
|
+
|
|
134
|
+
## Views
|
|
135
|
+
|
|
136
|
+
| View | Route suffix | Description |
|
|
137
|
+
|------|-------------|-------------|
|
|
138
|
+
| Dashboard | `/inventory` | KPIs: total products, low stock, stock value |
|
|
139
|
+
| Products List | `/inventory/products/list` | Filterable by type |
|
|
140
|
+
| Products New | `/inventory/products/new` | Create product |
|
|
141
|
+
| Stock Entry | `/inventory/stock/entry` | Record goods received |
|
|
142
|
+
| Stock Exit | `/inventory/stock/exit` | Record goods consumed |
|
|
143
|
+
| Stock History | `/inventory/stock/history` | Movement log |
|
|
144
|
+
| Recipes | `/inventory/recipes/list` | Production formulas |
|
|
145
|
+
| Settings | `/inventory/settings/*` | Registry management |
|
|
146
|
+
|
|
147
|
+
## Extending
|
|
148
|
+
|
|
149
|
+
### Custom Data Provider
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import type { InventoryDataProvider } from '@fayz-ai/saas-core/plugins/inventory'
|
|
153
|
+
|
|
154
|
+
const supabaseProvider: InventoryDataProvider = {
|
|
155
|
+
async getProducts(query) { /* Supabase query */ },
|
|
156
|
+
async createMovement(input) { /* Supabase insert + trigger */ },
|
|
157
|
+
// ... implement all methods
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
createInventoryPlugin({ dataProvider: supabaseProvider })
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Future: Unit Conversion System
|
|
164
|
+
|
|
165
|
+
The types support multi-unit products (base unit, content unit, purchase unit) following the beautyplace pattern. Example:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
Shampoo bottle (base: UNIT)
|
|
169
|
+
- Content: 300mL per unit
|
|
170
|
+
- Purchase: BOX of 12 units
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This will be implemented when the Supabase provider is built.
|
|
174
|
+
|
|
175
|
+
### Future: DANFE/NF-e Import
|
|
176
|
+
|
|
177
|
+
Invoice XML import for automatic stock entry — will be added as a sub-module.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { SettingsGroup, ToggleRow } from '@fayz-ai/saas'
|
|
3
|
+
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
+
|
|
5
|
+
export function InventoryGeneralSettings() {
|
|
6
|
+
const t = useTranslation()
|
|
7
|
+
return (
|
|
8
|
+
<div className="space-y-4">
|
|
9
|
+
<SettingsGroup title={t('inventory.settings.stockManagement')} description={t('inventory.settings.stockManagementDesc')}>
|
|
10
|
+
<ToggleRow label={t('inventory.settings.lowStockAlerts')} description={t('inventory.settings.lowStockAlertsDesc')} checked={true} onChange={() => {}} />
|
|
11
|
+
<ToggleRow label={t('inventory.settings.requireReason')} description={t('inventory.settings.requireReasonDesc')} checked={true} onChange={() => {}} />
|
|
12
|
+
<ToggleRow label={t('inventory.settings.autoDeduct')} description={t('inventory.settings.autoDeductDesc')} checked={false} onChange={() => {}} />
|
|
13
|
+
</SettingsGroup>
|
|
14
|
+
|
|
15
|
+
<SettingsGroup title={t('inventory.settings.products')} description={t('inventory.settings.productsDesc')}>
|
|
16
|
+
<ToggleRow label={t('inventory.settings.requireSku')} description={t('inventory.settings.requireSkuDesc')} checked={false} onChange={() => {}} />
|
|
17
|
+
<ToggleRow label={t('inventory.settings.allowNegative')} description={t('inventory.settings.allowNegativeDesc')} checked={false} onChange={() => {}} />
|
|
18
|
+
</SettingsGroup>
|
|
19
|
+
|
|
20
|
+
<SettingsGroup title={t('inventory.settings.notifications')} description={t('inventory.settings.notificationsDesc')}>
|
|
21
|
+
<ToggleRow label={t('inventory.settings.lowStockEmail')} description={t('inventory.settings.lowStockEmailDesc')} checked={false} onChange={() => {}} />
|
|
22
|
+
<ToggleRow label={t('inventory.settings.expiryWarnings')} description={t('inventory.settings.expiryWarningsDesc')} checked={true} onChange={() => {}} />
|
|
23
|
+
</SettingsGroup>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Package, Ruler, MapPin, ChevronRight, Check } from 'lucide-react'
|
|
3
|
+
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
+
import { Button } from '@fayz-ai/ui'
|
|
5
|
+
|
|
6
|
+
const STEPS = [
|
|
7
|
+
{ id: 'welcome', icon: Package, titleKey: 'inventory.onboarding.welcome', descKey: 'inventory.onboarding.description' },
|
|
8
|
+
{ id: 'units', icon: Ruler, titleKey: 'inventory.onboarding.units.title', descKey: 'inventory.onboarding.units.description' },
|
|
9
|
+
{ id: 'locations', icon: MapPin, titleKey: 'inventory.onboarding.locations.title', descKey: 'inventory.onboarding.locations.description' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export function InventoryOnboarding({ onComplete }: { onComplete: () => void }) {
|
|
13
|
+
const t = useTranslation()
|
|
14
|
+
const [step, setStep] = useState(0)
|
|
15
|
+
const current = STEPS[step]
|
|
16
|
+
const isLast = step === STEPS.length - 1
|
|
17
|
+
const Icon = current.icon
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex items-center justify-center py-12">
|
|
21
|
+
<div className="w-full max-w-lg">
|
|
22
|
+
<div className="flex items-center gap-1 mb-8">
|
|
23
|
+
{STEPS.map((_, i) => (
|
|
24
|
+
<div key={i} className={`h-1 flex-1 rounded-full transition-colors ${i <= step ? 'bg-primary' : 'bg-muted'}`} />
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
<div className="rounded-card border bg-card p-8 text-center space-y-6 shadow-sm">
|
|
28
|
+
<div className="flex justify-center">
|
|
29
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
|
|
30
|
+
<Icon className="h-8 w-8 text-primary" />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div>
|
|
34
|
+
<h2 className="text-xl font-bold">{t(current.titleKey)}</h2>
|
|
35
|
+
<p className="text-sm text-muted-foreground mt-2 max-w-sm mx-auto">{t(current.descKey)}</p>
|
|
36
|
+
</div>
|
|
37
|
+
<div className="flex items-center justify-center gap-3 pt-2">
|
|
38
|
+
{step > 0 && (
|
|
39
|
+
<Button variant="outline" size="lg" onClick={() => setStep(step - 1)}>{t('common.back')}</Button>
|
|
40
|
+
)}
|
|
41
|
+
{isLast ? (
|
|
42
|
+
<Button variant="default" size="lg" onClick={onComplete}>
|
|
43
|
+
<Check className="h-4 w-4" /> {t('inventory.onboarding.start')}
|
|
44
|
+
</Button>
|
|
45
|
+
) : (
|
|
46
|
+
<Button variant="default" size="lg" onClick={() => setStep(step + 1)}>
|
|
47
|
+
{step === 0 ? t('inventory.onboarding.getStarted') : t('common.continue')} <ChevronRight className="h-4 w-4" />
|
|
48
|
+
</Button>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
{step === 0 && (
|
|
52
|
+
<button onClick={onComplete} className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
|
53
|
+
{t('inventory.onboarding.skip')}
|
|
54
|
+
</button>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { ChevronLeft } from 'lucide-react'
|
|
3
|
+
import { PluginRegistryManager } from '@fayz-ai/saas'
|
|
4
|
+
import type { PluginRegistryDef } from '@fayz-ai/core'
|
|
5
|
+
import { useTranslation } from '@fayz-ai/core'
|
|
6
|
+
|
|
7
|
+
export function InventorySettings({ registries, routeBase, onClose }: {
|
|
8
|
+
registries: PluginRegistryDef[]
|
|
9
|
+
routeBase: string
|
|
10
|
+
onClose: () => void
|
|
11
|
+
}) {
|
|
12
|
+
const t = useTranslation()
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-4">
|
|
15
|
+
<div className="flex items-center gap-3">
|
|
16
|
+
<button onClick={onClose} className="flex h-8 w-8 items-center justify-center rounded-lg border hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors">
|
|
17
|
+
<ChevronLeft className="h-4 w-4" />
|
|
18
|
+
</button>
|
|
19
|
+
<div>
|
|
20
|
+
<h2 className="text-lg font-bold">{t('inventory.settingsPage.title')}</h2>
|
|
21
|
+
<p className="text-xs text-muted-foreground">{t('inventory.settingsPage.subtitle')}</p>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<PluginRegistryManager registries={registries} routeBase={routeBase} />
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|