@fayz-ai/plugin-inventory 0.9.0 → 0.9.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 +3 -3
- package/dist/{InventoryContext.d.ts → context.d.ts} +2 -2
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/{InventoryPage.d.ts → views/InventoryPage.d.ts} +3 -3
- package/dist/views/InventoryPage.d.ts.map +1 -0
- package/dist/views/ProductCrudForm.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/dist/views/productEntity.d.ts +1 -1
- package/dist/views/productEntity.d.ts.map +1 -1
- package/package.json +5 -7
- package/dist/InventoryContext.d.ts.map +0 -1
- package/dist/InventoryPage.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/InventoryContext.tsx +0 -42
- package/src/InventoryPage.tsx +0 -176
- package/src/README.md +0 -177
- package/src/components/InventoryGeneralSettings.tsx +0 -39
- package/src/components/InventoryOnboarding.tsx +0 -60
- package/src/components/InventorySettings.tsx +0 -27
- package/src/data/index.ts +0 -2
- package/src/data/mock.ts +0 -266
- package/src/data/supabase.ts +0 -361
- package/src/data/tables.ts +0 -10
- package/src/data/types.ts +0 -35
- package/src/index.ts +0 -238
- package/src/lib/tenant.ts +0 -4
- package/src/locales/en.ts +0 -242
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -242
- package/src/migrations/000_plg_rename.sql +0 -27
- package/src/migrations/001_inventory_base.sql +0 -69
- package/src/migrations/002_recipes.sql +0 -34
- package/src/migrations/003_measurement_units.sql +0 -13
- package/src/migrations/index.ts +0 -161
- package/src/registries.ts +0 -111
- package/src/store.ts +0 -127
- package/src/types.ts +0 -256
- package/src/views/DashboardView.tsx +0 -11
- package/src/views/MovementHistoryView.tsx +0 -104
- package/src/views/ProductCrudForm.tsx +0 -104
- package/src/views/ProductListView.tsx +0 -58
- package/src/views/RecipeDetailView.tsx +0 -192
- package/src/views/RecipeFormView.tsx +0 -241
- package/src/views/RecipesView.tsx +0 -106
- package/src/views/StockMovementView.tsx +0 -518
- package/src/views/dashboardWidgets.tsx +0 -101
- package/src/views/productEntity.tsx +0 -124
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
package/src/registries.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import type { PluginRegistryDef } from '@fayz-ai/core'
|
|
2
|
-
import type { EntityDef } from '@fayz-ai/core'
|
|
3
|
-
import { T } from './data/tables'
|
|
4
|
-
|
|
5
|
-
const supplierEntity: EntityDef = {
|
|
6
|
-
name: 'Supplier',
|
|
7
|
-
namePlural: 'Suppliers',
|
|
8
|
-
icon: 'Building2',
|
|
9
|
-
layout: 'person',
|
|
10
|
-
displayField: 'name',
|
|
11
|
-
defaultSort: 'name',
|
|
12
|
-
fields: [
|
|
13
|
-
{ key: 'name', label: 'Name', type: 'text', required: true, showInTable: true, searchable: true },
|
|
14
|
-
{ key: 'phone', label: 'Phone', type: 'phone', showInTable: true },
|
|
15
|
-
{ key: 'email', label: 'Email', type: 'email', showInTable: true },
|
|
16
|
-
{ key: 'documentNumber', label: 'Tax ID', type: 'text', showInTable: true },
|
|
17
|
-
{ key: 'address', label: 'Address', type: 'text' },
|
|
18
|
-
{ key: 'notes', label: 'Notes', type: 'textarea' },
|
|
19
|
-
{ key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
|
|
20
|
-
],
|
|
21
|
-
data: {
|
|
22
|
-
table: 'people',
|
|
23
|
-
tenantScoped: true,
|
|
24
|
-
filters: { kind: 'supplier' },
|
|
25
|
-
defaults: { kind: 'supplier' },
|
|
26
|
-
},
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const measurementUnitEntity: EntityDef = {
|
|
30
|
-
name: 'Measurement Unit',
|
|
31
|
-
namePlural: 'Measurement Units',
|
|
32
|
-
icon: 'Ruler',
|
|
33
|
-
displayField: 'name',
|
|
34
|
-
defaultSort: 'name',
|
|
35
|
-
fields: [
|
|
36
|
-
{ key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
|
|
37
|
-
{ key: 'abbreviation', label: 'Abbreviation', type: 'text', required: true, showInTable: true },
|
|
38
|
-
{ key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
|
|
39
|
-
],
|
|
40
|
-
data: { table: T.measurementUnits, tenantScoped: true },
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const productCategoryEntity: EntityDef = {
|
|
44
|
-
name: 'Product Category',
|
|
45
|
-
namePlural: 'Product Categories',
|
|
46
|
-
icon: 'Tag',
|
|
47
|
-
displayField: 'name',
|
|
48
|
-
defaultSort: 'name',
|
|
49
|
-
fields: [
|
|
50
|
-
{ key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
|
|
51
|
-
{ key: 'parentId', label: 'Parent', type: 'text', showInTable: false },
|
|
52
|
-
{ key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
|
|
53
|
-
],
|
|
54
|
-
data: { table: T.productCategories, tenantScoped: true },
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const stockLocationEntity: EntityDef = {
|
|
58
|
-
name: 'Stock Location',
|
|
59
|
-
namePlural: 'Stock Locations',
|
|
60
|
-
icon: 'Warehouse',
|
|
61
|
-
displayField: 'name',
|
|
62
|
-
defaultSort: 'name',
|
|
63
|
-
fields: [
|
|
64
|
-
{ key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
|
|
65
|
-
{ key: 'description', label: 'Description', type: 'textarea', showInTable: true },
|
|
66
|
-
{ key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
|
|
67
|
-
],
|
|
68
|
-
data: { table: T.stockLocations, tenantScoped: true },
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export const inventoryRegistries: PluginRegistryDef[] = [
|
|
72
|
-
// --- Editable ---
|
|
73
|
-
{
|
|
74
|
-
id: 'suppliers',
|
|
75
|
-
entity: supplierEntity,
|
|
76
|
-
icon: 'Building2',
|
|
77
|
-
description: 'Manage your product and material suppliers',
|
|
78
|
-
display: 'table',
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: 'product-categories',
|
|
82
|
-
entity: productCategoryEntity,
|
|
83
|
-
icon: 'Tag',
|
|
84
|
-
description: 'Product categories for organization',
|
|
85
|
-
display: 'table',
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: 'stock-locations',
|
|
89
|
-
entity: stockLocationEntity,
|
|
90
|
-
icon: 'Warehouse',
|
|
91
|
-
description: 'Physical storage locations for inventory',
|
|
92
|
-
display: 'table',
|
|
93
|
-
},
|
|
94
|
-
// --- Read-only (system / seeded) ---
|
|
95
|
-
{
|
|
96
|
-
id: 'measurement-units',
|
|
97
|
-
entity: measurementUnitEntity,
|
|
98
|
-
icon: 'Ruler',
|
|
99
|
-
description: 'Standard units of measure',
|
|
100
|
-
display: 'table',
|
|
101
|
-
readOnly: true,
|
|
102
|
-
seedData: [
|
|
103
|
-
{ id: 'mu-unit', name: 'Unit', abbreviation: 'un', isActive: true },
|
|
104
|
-
{ id: 'mu-box', name: 'Box', abbreviation: 'box', isActive: true },
|
|
105
|
-
{ id: 'mu-kg', name: 'Kilogram', abbreviation: 'kg', isActive: true },
|
|
106
|
-
{ id: 'mu-g', name: 'Gram', abbreviation: 'g', isActive: true },
|
|
107
|
-
{ id: 'mu-l', name: 'Liter', abbreviation: 'L', isActive: true },
|
|
108
|
-
{ id: 'mu-ml', name: 'Milliliter', abbreviation: 'mL', isActive: true },
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
]
|
package/src/store.ts
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { createStore, type StoreApi } from 'zustand/vanilla'
|
|
2
|
-
import { dedup } from '@fayz-ai/saas'
|
|
3
|
-
import { toast } from '@fayz-ai/ui'
|
|
4
|
-
import type { InventoryDataProvider } from './data/types'
|
|
5
|
-
import type {
|
|
6
|
-
Product, StockMovement, StockLocation, Recipe,
|
|
7
|
-
InventorySummary, ProductQuery, MovementQuery,
|
|
8
|
-
CreateProductInput, CreateStockMovementInput, CreateRecipeInput,
|
|
9
|
-
} from './types'
|
|
10
|
-
|
|
11
|
-
export interface InventoryUIState {
|
|
12
|
-
products: Product[]
|
|
13
|
-
productsTotal: number
|
|
14
|
-
productsLoading: boolean
|
|
15
|
-
productQuery: ProductQuery
|
|
16
|
-
|
|
17
|
-
movements: StockMovement[]
|
|
18
|
-
movementsTotal: number
|
|
19
|
-
movementsLoading: boolean
|
|
20
|
-
|
|
21
|
-
locations: StockLocation[]
|
|
22
|
-
locationsLoading: boolean
|
|
23
|
-
|
|
24
|
-
recipes: Recipe[]
|
|
25
|
-
recipesLoading: boolean
|
|
26
|
-
|
|
27
|
-
summary: InventorySummary | null
|
|
28
|
-
summaryLoading: boolean
|
|
29
|
-
|
|
30
|
-
fetchSummary(): Promise<void>
|
|
31
|
-
fetchProducts(query: ProductQuery): Promise<void>
|
|
32
|
-
fetchMovements(query: MovementQuery): Promise<void>
|
|
33
|
-
fetchLocations(): Promise<void>
|
|
34
|
-
fetchRecipes(): Promise<void>
|
|
35
|
-
createProduct(input: CreateProductInput): Promise<Product>
|
|
36
|
-
createMovement(input: CreateStockMovementInput): Promise<StockMovement>
|
|
37
|
-
createRecipe(input: CreateRecipeInput): Promise<Recipe>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function createInventoryStore(provider: InventoryDataProvider): StoreApi<InventoryUIState> {
|
|
41
|
-
return createStore<InventoryUIState>((set, get) => ({
|
|
42
|
-
products: [], productsTotal: 0, productsLoading: false, productQuery: {},
|
|
43
|
-
movements: [], movementsTotal: 0, movementsLoading: false,
|
|
44
|
-
locations: [], locationsLoading: false,
|
|
45
|
-
recipes: [], recipesLoading: false,
|
|
46
|
-
summary: null, summaryLoading: false,
|
|
47
|
-
|
|
48
|
-
async fetchSummary() {
|
|
49
|
-
return dedup('inv:summary', async () => {
|
|
50
|
-
set({ summaryLoading: true })
|
|
51
|
-
const summary = await provider.getSummary()
|
|
52
|
-
set({ summary, summaryLoading: false })
|
|
53
|
-
})
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
async fetchProducts(query) {
|
|
57
|
-
const key = 'inv:products:' + JSON.stringify(query)
|
|
58
|
-
return dedup(key, async () => {
|
|
59
|
-
set({ productsLoading: true, productQuery: query })
|
|
60
|
-
const result = await provider.getProducts(query)
|
|
61
|
-
set({ products: result.data, productsTotal: result.total, productsLoading: false })
|
|
62
|
-
})
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
async fetchMovements(query) {
|
|
66
|
-
return dedup('inv:movements:' + JSON.stringify(query), async () => {
|
|
67
|
-
set({ movementsLoading: true })
|
|
68
|
-
const result = await provider.getMovements(query)
|
|
69
|
-
set({ movements: result.data, movementsTotal: result.total, movementsLoading: false })
|
|
70
|
-
})
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
async fetchLocations() {
|
|
74
|
-
return dedup('inv:locations', async () => {
|
|
75
|
-
set({ locationsLoading: true })
|
|
76
|
-
const locations = await provider.getLocations()
|
|
77
|
-
set({ locations, locationsLoading: false })
|
|
78
|
-
})
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
async fetchRecipes() {
|
|
82
|
-
set({ recipesLoading: true })
|
|
83
|
-
const recipes = await provider.getRecipes()
|
|
84
|
-
set({ recipes, recipesLoading: false })
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
async createProduct(input) {
|
|
88
|
-
try {
|
|
89
|
-
const product = await provider.createProduct(input)
|
|
90
|
-
const query = get().productQuery
|
|
91
|
-
const [result, summary] = await Promise.all([provider.getProducts(query), provider.getSummary()])
|
|
92
|
-
set({ products: result.data, productsTotal: result.total, summary })
|
|
93
|
-
toast.success('Product created')
|
|
94
|
-
return product
|
|
95
|
-
} catch (err: any) {
|
|
96
|
-
toast.error('Failed to create product', { description: err?.message })
|
|
97
|
-
throw err
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
async createMovement(input) {
|
|
102
|
-
try {
|
|
103
|
-
const movement = await provider.createMovement(input)
|
|
104
|
-
const [summary] = await Promise.all([provider.getSummary()])
|
|
105
|
-
set({ summary })
|
|
106
|
-
toast.success('Stock movement recorded')
|
|
107
|
-
return movement
|
|
108
|
-
} catch (err: any) {
|
|
109
|
-
toast.error('Failed to record movement', { description: err?.message })
|
|
110
|
-
throw err
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
async createRecipe(input) {
|
|
115
|
-
try {
|
|
116
|
-
const recipe = await provider.createRecipe(input)
|
|
117
|
-
const recipes = await provider.getRecipes()
|
|
118
|
-
set({ recipes })
|
|
119
|
-
toast.success('Recipe created')
|
|
120
|
-
return recipe
|
|
121
|
-
} catch (err: any) {
|
|
122
|
-
toast.error('Failed to create recipe', { description: err?.message })
|
|
123
|
-
throw err
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
}))
|
|
127
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Inventory Plugin — Pure TypeScript types
|
|
3
|
-
// Zero dependencies. Abstracted from beautyplace Estoque module.
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
// ============================================================
|
|
7
|
-
// ENUMS / LITERALS
|
|
8
|
-
// ============================================================
|
|
9
|
-
|
|
10
|
-
/** Product classification — a product can have multiple types */
|
|
11
|
-
export type ProductType = 'ingredient' | 'sale' | 'intermediate' | 'asset'
|
|
12
|
-
|
|
13
|
-
/** Stock movement direction/reason */
|
|
14
|
-
export type MovementType = 'entry' | 'exit' | 'adjustment' | 'transfer' | 'loss'
|
|
15
|
-
|
|
16
|
-
/** Product purpose for filtering */
|
|
17
|
-
export type ProductPurpose = 'purchase' | 'sale' | 'both' | 'internal'
|
|
18
|
-
|
|
19
|
-
// ============================================================
|
|
20
|
-
// CORE ENTITIES
|
|
21
|
-
// ============================================================
|
|
22
|
-
|
|
23
|
-
export interface Product {
|
|
24
|
-
id: string
|
|
25
|
-
name: string
|
|
26
|
-
description?: string
|
|
27
|
-
sku?: string
|
|
28
|
-
barcode?: string
|
|
29
|
-
brand?: string
|
|
30
|
-
categoryId?: string
|
|
31
|
-
categoryName?: string
|
|
32
|
-
productType: ProductType
|
|
33
|
-
purpose?: ProductPurpose
|
|
34
|
-
currentQuantity: number
|
|
35
|
-
minQuantity: number
|
|
36
|
-
maxQuantity?: number
|
|
37
|
-
costPrice: number
|
|
38
|
-
salePrice?: number
|
|
39
|
-
measurementUnitId?: string
|
|
40
|
-
measurementUnitName?: string
|
|
41
|
-
isActive: boolean
|
|
42
|
-
supplierId?: string
|
|
43
|
-
supplierName?: string
|
|
44
|
-
defaultLocationId?: string
|
|
45
|
-
imageUrl?: string
|
|
46
|
-
metadata?: Record<string, unknown>
|
|
47
|
-
tenantId: string
|
|
48
|
-
createdAt: string
|
|
49
|
-
updatedAt: string
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface StockMovement {
|
|
53
|
-
id: string
|
|
54
|
-
productId: string
|
|
55
|
-
productName?: string
|
|
56
|
-
quantity: number
|
|
57
|
-
movementType: MovementType
|
|
58
|
-
unitCost: number
|
|
59
|
-
totalCost: number
|
|
60
|
-
stockLocationId?: string
|
|
61
|
-
stockLocationName?: string
|
|
62
|
-
destinationLocationId?: string
|
|
63
|
-
destinationLocationName?: string
|
|
64
|
-
supplierId?: string
|
|
65
|
-
supplierName?: string
|
|
66
|
-
documentNumber?: string
|
|
67
|
-
reason?: string
|
|
68
|
-
notes?: string
|
|
69
|
-
movementDate: string
|
|
70
|
-
userId?: string
|
|
71
|
-
userName?: string
|
|
72
|
-
metadata?: Record<string, unknown>
|
|
73
|
-
tenantId: string
|
|
74
|
-
createdAt: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface StockPosition {
|
|
78
|
-
id: string
|
|
79
|
-
productId: string
|
|
80
|
-
productName?: string
|
|
81
|
-
quantity: number
|
|
82
|
-
unitCost: number
|
|
83
|
-
stockLocationId?: string
|
|
84
|
-
stockLocationName?: string
|
|
85
|
-
batchNumber?: string
|
|
86
|
-
expirationDate?: string
|
|
87
|
-
tenantId: string
|
|
88
|
-
createdAt: string
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface StockLocation {
|
|
92
|
-
id: string
|
|
93
|
-
name: string
|
|
94
|
-
description?: string
|
|
95
|
-
isActive: boolean
|
|
96
|
-
unitId?: string
|
|
97
|
-
tenantId: string
|
|
98
|
-
createdAt: string
|
|
99
|
-
updatedAt: string
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export interface Recipe {
|
|
103
|
-
id: string
|
|
104
|
-
name: string
|
|
105
|
-
description?: string
|
|
106
|
-
productId: string
|
|
107
|
-
productName?: string
|
|
108
|
-
yieldQuantity: number
|
|
109
|
-
yieldUnitId?: string
|
|
110
|
-
yieldUnitName?: string
|
|
111
|
-
preparationTimeMinutes?: number
|
|
112
|
-
instructions?: string
|
|
113
|
-
isActive: boolean
|
|
114
|
-
ingredientCount?: number
|
|
115
|
-
tenantId: string
|
|
116
|
-
createdAt: string
|
|
117
|
-
updatedAt: string
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface RecipeIngredient {
|
|
121
|
-
id: string
|
|
122
|
-
recipeId: string
|
|
123
|
-
productId: string
|
|
124
|
-
productName?: string
|
|
125
|
-
quantity: number
|
|
126
|
-
unitId?: string
|
|
127
|
-
unitName?: string
|
|
128
|
-
displayOrder: number
|
|
129
|
-
notes?: string
|
|
130
|
-
createdAt: string
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export interface MeasurementUnit {
|
|
134
|
-
id: string
|
|
135
|
-
name: string
|
|
136
|
-
abbreviation: string
|
|
137
|
-
isActive: boolean
|
|
138
|
-
tenantId: string
|
|
139
|
-
createdAt: string
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface ProductCategory {
|
|
143
|
-
id: string
|
|
144
|
-
name: string
|
|
145
|
-
parentId?: string
|
|
146
|
-
isActive: boolean
|
|
147
|
-
tenantId: string
|
|
148
|
-
createdAt: string
|
|
149
|
-
updatedAt: string
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ============================================================
|
|
153
|
-
// INPUT TYPES
|
|
154
|
-
// ============================================================
|
|
155
|
-
|
|
156
|
-
export interface CreateProductInput {
|
|
157
|
-
name: string
|
|
158
|
-
description?: string
|
|
159
|
-
sku?: string
|
|
160
|
-
barcode?: string
|
|
161
|
-
brand?: string
|
|
162
|
-
categoryId?: string
|
|
163
|
-
productType: ProductType
|
|
164
|
-
purpose?: ProductPurpose
|
|
165
|
-
minQuantity?: number
|
|
166
|
-
maxQuantity?: number
|
|
167
|
-
costPrice?: number
|
|
168
|
-
salePrice?: number
|
|
169
|
-
measurementUnitId?: string
|
|
170
|
-
supplierId?: string
|
|
171
|
-
defaultLocationId?: string
|
|
172
|
-
imageUrl?: string
|
|
173
|
-
metadata?: Record<string, unknown>
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export interface CreateStockMovementInput {
|
|
177
|
-
productId: string
|
|
178
|
-
quantity: number
|
|
179
|
-
movementType: MovementType
|
|
180
|
-
unitCost?: number
|
|
181
|
-
stockLocationId?: string
|
|
182
|
-
destinationLocationId?: string
|
|
183
|
-
supplierId?: string
|
|
184
|
-
documentNumber?: string
|
|
185
|
-
reason?: string
|
|
186
|
-
notes?: string
|
|
187
|
-
movementDate?: string
|
|
188
|
-
batchNumber?: string
|
|
189
|
-
expirationDate?: string
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export interface CreateRecipeInput {
|
|
193
|
-
name: string
|
|
194
|
-
description?: string
|
|
195
|
-
productId: string
|
|
196
|
-
yieldQuantity: number
|
|
197
|
-
yieldUnitId?: string
|
|
198
|
-
preparationTimeMinutes?: number
|
|
199
|
-
instructions?: string
|
|
200
|
-
ingredients: CreateRecipeIngredientInput[]
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export interface CreateRecipeIngredientInput {
|
|
204
|
-
productId: string
|
|
205
|
-
quantity: number
|
|
206
|
-
unitId?: string
|
|
207
|
-
displayOrder?: number
|
|
208
|
-
notes?: string
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ============================================================
|
|
212
|
-
// QUERY / FILTER TYPES
|
|
213
|
-
// ============================================================
|
|
214
|
-
|
|
215
|
-
export interface DateRange {
|
|
216
|
-
from: string
|
|
217
|
-
to: string
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export interface ProductQuery {
|
|
221
|
-
productType?: ProductType
|
|
222
|
-
categoryId?: string
|
|
223
|
-
search?: string
|
|
224
|
-
lowStockOnly?: boolean
|
|
225
|
-
isActive?: boolean
|
|
226
|
-
page?: number
|
|
227
|
-
pageSize?: number
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export interface MovementQuery {
|
|
231
|
-
productId?: string
|
|
232
|
-
movementType?: MovementType | MovementType[]
|
|
233
|
-
stockLocationId?: string
|
|
234
|
-
dateRange?: DateRange
|
|
235
|
-
search?: string
|
|
236
|
-
page?: number
|
|
237
|
-
pageSize?: number
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export interface PaginatedResult<T> {
|
|
241
|
-
data: T[]
|
|
242
|
-
total: number
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ============================================================
|
|
246
|
-
// AGGREGATION TYPES
|
|
247
|
-
// ============================================================
|
|
248
|
-
|
|
249
|
-
export interface InventorySummary {
|
|
250
|
-
totalProducts: number
|
|
251
|
-
lowStockCount: number
|
|
252
|
-
outOfStockCount: number
|
|
253
|
-
totalStockValue: number
|
|
254
|
-
recentMovementCount: number
|
|
255
|
-
movementsByType: Record<MovementType, number>
|
|
256
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { DashboardCanvas } from '@fayz-ai/ui'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Inventory overview. KPIs and the activity panel are registered dashboard
|
|
6
|
-
* widgets (see ./dashboardWidgets), rendered through the shared DashboardCanvas;
|
|
7
|
-
* the stock-value KPI also surfaces on the global app home.
|
|
8
|
-
*/
|
|
9
|
-
export function DashboardView() {
|
|
10
|
-
return <DashboardCanvas surface="plugin-home" domain="inventory" showHeader={false} className="space-y-6" />
|
|
11
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState, useMemo } from 'react'
|
|
2
|
-
import { ArrowUpRight, ArrowDownRight, RefreshCw, ArrowRightLeft, Trash2 } from 'lucide-react'
|
|
3
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
4
|
-
import { ListView } from '@fayz-ai/ui'
|
|
5
|
-
import { useInventoryStore, formatCurrency, useInventoryConfig } from '../InventoryContext'
|
|
6
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
7
|
-
import { SubpageHeader } from '@fayz-ai/ui'
|
|
8
|
-
import type { MovementType, StockMovement } from '../types'
|
|
9
|
-
|
|
10
|
-
const TYPE_ICONS: Record<MovementType, React.ElementType> = {
|
|
11
|
-
entry: ArrowDownRight,
|
|
12
|
-
exit: ArrowUpRight,
|
|
13
|
-
adjustment: RefreshCw,
|
|
14
|
-
transfer: ArrowRightLeft,
|
|
15
|
-
loss: Trash2,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const TYPE_COLORS: Record<MovementType, string> = {
|
|
19
|
-
entry: 'text-success',
|
|
20
|
-
exit: 'text-destructive',
|
|
21
|
-
adjustment: 'text-info',
|
|
22
|
-
transfer: 'text-magic',
|
|
23
|
-
loss: 'text-warning',
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function MovementHistoryView({ onViewDetail }: { onViewDetail?: (id: string) => void } = {}) {
|
|
27
|
-
const t = useTranslation()
|
|
28
|
-
const { currency } = useInventoryConfig()
|
|
29
|
-
const movements = useInventoryStore((s) => s.movements)
|
|
30
|
-
const movementsLoading = useInventoryStore((s) => s.movementsLoading)
|
|
31
|
-
const fetchMovements = useInventoryStore((s) => s.fetchMovements)
|
|
32
|
-
|
|
33
|
-
const [search, setSearch] = useState('')
|
|
34
|
-
|
|
35
|
-
useEffect(() => { fetchMovements({ search: search || undefined }) }, [search])
|
|
36
|
-
|
|
37
|
-
const columns: ColumnDef<StockMovement, any>[] = useMemo(() => [
|
|
38
|
-
{
|
|
39
|
-
accessorKey: 'movementDate', header: t('inventory.history.columnDate'),
|
|
40
|
-
cell: ({ getValue }: any) => <span className="text-xs text-muted-foreground">{getValue() as string}</span>,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
accessorKey: 'productName', header: t('inventory.history.columnProduct'),
|
|
44
|
-
cell: ({ getValue }: any) => <span className="font-medium">{(getValue() as string) ?? '—'}</span>,
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
accessorKey: 'movementType', header: t('inventory.history.columnType'),
|
|
48
|
-
cell: ({ getValue }: any) => {
|
|
49
|
-
const type = getValue() as MovementType
|
|
50
|
-
const Icon = TYPE_ICONS[type] ?? RefreshCw
|
|
51
|
-
const color = TYPE_COLORS[type] ?? 'text-muted-foreground'
|
|
52
|
-
return (
|
|
53
|
-
<span className={`inline-flex items-center gap-1 ${color}`}>
|
|
54
|
-
<Icon className="h-3 w-3" />
|
|
55
|
-
<span className="text-xs capitalize">{type}</span>
|
|
56
|
-
</span>
|
|
57
|
-
)
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 'location', header: t('inventory.history.columnLocation'),
|
|
62
|
-
cell: ({ row }: any) => (
|
|
63
|
-
<span className="text-xs text-muted-foreground">
|
|
64
|
-
{row.original.stockLocationName ?? '—'}
|
|
65
|
-
{row.original.movementType === 'transfer' && row.original.destinationLocationName && (
|
|
66
|
-
<span> → {row.original.destinationLocationName}</span>
|
|
67
|
-
)}
|
|
68
|
-
</span>
|
|
69
|
-
),
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
accessorKey: 'quantity', header: t('inventory.history.columnQty'),
|
|
73
|
-
cell: ({ row }: any) => (
|
|
74
|
-
<span className="text-right block">
|
|
75
|
-
{row.original.movementType === 'entry' ? '+' : '-'}{row.original.quantity}
|
|
76
|
-
</span>
|
|
77
|
-
),
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
accessorKey: 'totalCost', header: t('inventory.history.columnTotal'),
|
|
81
|
-
cell: ({ getValue }: any) => (
|
|
82
|
-
<span className="text-right block text-muted-foreground">{formatCurrency(getValue() as number, currency)}</span>
|
|
83
|
-
),
|
|
84
|
-
},
|
|
85
|
-
], [currency, t])
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className="space-y-4">
|
|
89
|
-
<SubpageHeader title={t('inventory.history.title')} subtitle={t('inventory.history.subtitle')} />
|
|
90
|
-
|
|
91
|
-
<ListView<StockMovement>
|
|
92
|
-
columns={columns}
|
|
93
|
-
data={movements}
|
|
94
|
-
loading={movementsLoading}
|
|
95
|
-
searchPlaceholder={t('inventory.history.searchPlaceholder')}
|
|
96
|
-
search={search}
|
|
97
|
-
onSearchChange={setSearch}
|
|
98
|
-
searchDebounce={0}
|
|
99
|
-
onRowClick={onViewDetail ? (row) => onViewDetail(row.id) : undefined}
|
|
100
|
-
emptyMessage={t('inventory.history.noMovements')}
|
|
101
|
-
/>
|
|
102
|
-
</div>
|
|
103
|
-
)
|
|
104
|
-
}
|