@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.
Files changed (93) hide show
  1. package/dist/InventoryContext.d.ts +37 -0
  2. package/dist/InventoryContext.d.ts.map +1 -0
  3. package/dist/InventoryPage.d.ts +13 -0
  4. package/dist/InventoryPage.d.ts.map +1 -0
  5. package/dist/components/InventoryGeneralSettings.d.ts +3 -0
  6. package/dist/components/InventoryGeneralSettings.d.ts.map +1 -0
  7. package/dist/components/InventoryOnboarding.d.ts +5 -0
  8. package/dist/components/InventoryOnboarding.d.ts.map +1 -0
  9. package/dist/components/InventorySettings.d.ts +8 -0
  10. package/dist/components/InventorySettings.d.ts.map +1 -0
  11. package/dist/data/index.d.ts +3 -0
  12. package/dist/data/index.d.ts.map +1 -0
  13. package/dist/data/mock.d.ts +3 -0
  14. package/dist/data/mock.d.ts.map +1 -0
  15. package/dist/data/supabase.d.ts +3 -0
  16. package/dist/data/supabase.d.ts.map +1 -0
  17. package/dist/data/types.d.ts +22 -0
  18. package/dist/data/types.d.ts.map +1 -0
  19. package/dist/index.cjs +2936 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.ts +48 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +2930 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lib/tenant.d.ts +3 -0
  26. package/dist/lib/tenant.d.ts.map +1 -0
  27. package/dist/locales/en.d.ts +2 -0
  28. package/dist/locales/en.d.ts.map +1 -0
  29. package/dist/locales/index.d.ts +2 -0
  30. package/dist/locales/index.d.ts.map +1 -0
  31. package/dist/locales/pt-BR.d.ts +2 -0
  32. package/dist/locales/pt-BR.d.ts.map +1 -0
  33. package/dist/registries.d.ts +3 -0
  34. package/dist/registries.d.ts.map +1 -0
  35. package/dist/store.d.ts +28 -0
  36. package/dist/store.d.ts.map +1 -0
  37. package/dist/types.d.ts +213 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/views/DashboardView.d.ts +8 -0
  40. package/dist/views/DashboardView.d.ts.map +1 -0
  41. package/dist/views/MovementHistoryView.d.ts +5 -0
  42. package/dist/views/MovementHistoryView.d.ts.map +1 -0
  43. package/dist/views/ProductCrudForm.d.ts +6 -0
  44. package/dist/views/ProductCrudForm.d.ts.map +1 -0
  45. package/dist/views/ProductFormView.d.ts +6 -0
  46. package/dist/views/ProductFormView.d.ts.map +1 -0
  47. package/dist/views/ProductListView.d.ts +6 -0
  48. package/dist/views/ProductListView.d.ts.map +1 -0
  49. package/dist/views/RecipeDetailView.d.ts +6 -0
  50. package/dist/views/RecipeDetailView.d.ts.map +1 -0
  51. package/dist/views/RecipeFormView.d.ts +5 -0
  52. package/dist/views/RecipeFormView.d.ts.map +1 -0
  53. package/dist/views/RecipesView.d.ts +6 -0
  54. package/dist/views/RecipesView.d.ts.map +1 -0
  55. package/dist/views/StockMovementView.d.ts +8 -0
  56. package/dist/views/StockMovementView.d.ts.map +1 -0
  57. package/dist/views/dashboardWidgets.d.ts +11 -0
  58. package/dist/views/dashboardWidgets.d.ts.map +1 -0
  59. package/dist/views/productEntity.d.ts +6 -0
  60. package/dist/views/productEntity.d.ts.map +1 -0
  61. package/package.json +55 -0
  62. package/src/InventoryContext.tsx +40 -0
  63. package/src/InventoryPage.tsx +170 -0
  64. package/src/README.md +177 -0
  65. package/src/components/InventoryGeneralSettings.tsx +26 -0
  66. package/src/components/InventoryOnboarding.tsx +60 -0
  67. package/src/components/InventorySettings.tsx +27 -0
  68. package/src/data/index.ts +2 -0
  69. package/src/data/mock.ts +266 -0
  70. package/src/data/supabase.ts +358 -0
  71. package/src/data/types.ts +35 -0
  72. package/src/index.ts +191 -0
  73. package/src/lib/tenant.ts +4 -0
  74. package/src/locales/en.ts +242 -0
  75. package/src/locales/index.ts +7 -0
  76. package/src/locales/pt-BR.ts +242 -0
  77. package/src/migrations/001_inventory_base.sql +69 -0
  78. package/src/migrations/002_recipes.sql +34 -0
  79. package/src/migrations/003_measurement_units.sql +13 -0
  80. package/src/registries.ts +111 -0
  81. package/src/store.ts +127 -0
  82. package/src/types.ts +256 -0
  83. package/src/views/DashboardView.tsx +11 -0
  84. package/src/views/MovementHistoryView.tsx +104 -0
  85. package/src/views/ProductCrudForm.tsx +99 -0
  86. package/src/views/ProductFormView.tsx +283 -0
  87. package/src/views/ProductListView.tsx +107 -0
  88. package/src/views/RecipeDetailView.tsx +192 -0
  89. package/src/views/RecipeFormView.tsx +235 -0
  90. package/src/views/RecipesView.tsx +103 -0
  91. package/src/views/StockMovementView.tsx +516 -0
  92. package/src/views/dashboardWidgets.tsx +101 -0
  93. package/src/views/productEntity.tsx +124 -0
package/src/types.ts ADDED
@@ -0,0 +1,256 @@
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
+ }
@@ -0,0 +1,11 @@
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
+ }
@@ -0,0 +1,104 @@
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
+ }
@@ -0,0 +1,99 @@
1
+ import React from 'react'
2
+ import { SubpageHeader, toast } from '@fayz-ai/ui'
3
+ import { useTranslation } from '@fayz-ai/core'
4
+ import { CrudFormPage } from '@fayz-ai/saas'
5
+ import { useInventoryConfig, useInventoryStore, useInventoryProvider } from '../InventoryContext'
6
+ import type { ProductType } from '../types'
7
+ import { buildProductEntity } from './productEntity'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Product create/edit, rendered through the generic CRUD form. The sectioned
11
+ // layout (General Information · Classification · Pricing+Margin · Stock Levels)
12
+ // comes from the shared buildProductEntity (segmented/computed/currency field
13
+ // types). Persistence still flows through the inventory provider/store, so the
14
+ // metadata-JSON column mapping is unchanged.
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export function ProductCrudForm({ editId, onSaved }: { editId?: string; onSaved?: () => void }) {
18
+ const t = useTranslation()
19
+ const { productTypes, currency } = useInventoryConfig()
20
+ const provider = useInventoryProvider()
21
+ const createProduct = useInventoryStore((s) => s.createProduct)
22
+ const isEdit = !!editId
23
+
24
+ const [initialData, setInitialData] = React.useState<Record<string, any> | null>(isEdit ? null : {})
25
+ const [headerName, setHeaderName] = React.useState('')
26
+
27
+ // Load the existing product for edit and map it to flat form values.
28
+ React.useEffect(() => {
29
+ if (!editId) return
30
+ let cancelled = false
31
+ ;(async () => {
32
+ const p = await provider.getProductById(editId)
33
+ if (cancelled) return
34
+ setHeaderName(p?.name ?? '')
35
+ setInitialData(
36
+ p
37
+ ? {
38
+ name: p.name,
39
+ brand: p.brand ?? '',
40
+ sku: p.sku ?? '',
41
+ barcode: p.barcode ?? '',
42
+ description: p.description ?? '',
43
+ productType: p.productType,
44
+ costPrice: p.costPrice,
45
+ salePrice: p.salePrice ?? 0,
46
+ minQuantity: p.minQuantity,
47
+ maxQuantity: p.maxQuantity ?? 0,
48
+ }
49
+ : {},
50
+ )
51
+ })()
52
+ return () => { cancelled = true }
53
+ }, [editId])
54
+
55
+ const entity = React.useMemo(() => buildProductEntity(t, productTypes, currency), [t, productTypes, currency])
56
+
57
+ async function save(values: Record<string, any>) {
58
+ const name = String(values.name ?? '').trim()
59
+ if (!name) { toast.error(t('common.formIncomplete')); throw new Error('name required') }
60
+ const payload = {
61
+ name,
62
+ sku: values.sku || undefined,
63
+ barcode: values.barcode || undefined,
64
+ brand: values.brand || undefined,
65
+ productType: (values.productType ?? productTypes[0]?.value) as ProductType,
66
+ costPrice: Number(values.costPrice) || 0,
67
+ salePrice: Number(values.salePrice) || undefined,
68
+ minQuantity: Number(values.minQuantity) || 0,
69
+ maxQuantity: Number(values.maxQuantity) || undefined,
70
+ description: values.description || undefined,
71
+ }
72
+ if (isEdit && editId) {
73
+ await provider.updateProduct(editId, payload)
74
+ } else {
75
+ await createProduct(payload)
76
+ }
77
+ onSaved?.()
78
+ }
79
+
80
+ const title = isEdit ? (headerName || t('inventory.productForm.editProduct')) : t('inventory.productForm.newProduct')
81
+ const subtitle = isEdit ? undefined : t('inventory.productForm.addToCatalog')
82
+
83
+ return (
84
+ <div className="space-y-5">
85
+ <SubpageHeader title={title} subtitle={subtitle} onBack={onSaved} parentLabel={t('inventory.nav.products')} />
86
+ {initialData && (
87
+ <CrudFormPage
88
+ entityDef={entity}
89
+ mode={isEdit ? 'edit' : 'create'}
90
+ initialData={initialData}
91
+ onSubmit={save}
92
+ onCancel={() => onSaved?.()}
93
+ namePlural={t('inventory.nav.products')}
94
+ hideBreadcrumb
95
+ />
96
+ )}
97
+ </div>
98
+ )
99
+ }