@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.
Files changed (54) hide show
  1. package/README.md +3 -3
  2. package/dist/{InventoryContext.d.ts → context.d.ts} +2 -2
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/data/registries.d.ts.map +1 -0
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +7 -3
  8. package/dist/index.js.map +1 -1
  9. package/dist/{InventoryPage.d.ts → views/InventoryPage.d.ts} +3 -3
  10. package/dist/views/InventoryPage.d.ts.map +1 -0
  11. package/dist/views/ProductCrudForm.d.ts.map +1 -1
  12. package/dist/views/dashboardWidgets.d.ts +1 -1
  13. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  14. package/dist/views/productEntity.d.ts +1 -1
  15. package/dist/views/productEntity.d.ts.map +1 -1
  16. package/package.json +5 -7
  17. package/dist/InventoryContext.d.ts.map +0 -1
  18. package/dist/InventoryPage.d.ts.map +0 -1
  19. package/dist/registries.d.ts.map +0 -1
  20. package/src/InventoryContext.tsx +0 -42
  21. package/src/InventoryPage.tsx +0 -176
  22. package/src/README.md +0 -177
  23. package/src/components/InventoryGeneralSettings.tsx +0 -39
  24. package/src/components/InventoryOnboarding.tsx +0 -60
  25. package/src/components/InventorySettings.tsx +0 -27
  26. package/src/data/index.ts +0 -2
  27. package/src/data/mock.ts +0 -266
  28. package/src/data/supabase.ts +0 -361
  29. package/src/data/tables.ts +0 -10
  30. package/src/data/types.ts +0 -35
  31. package/src/index.ts +0 -238
  32. package/src/lib/tenant.ts +0 -4
  33. package/src/locales/en.ts +0 -242
  34. package/src/locales/index.ts +0 -7
  35. package/src/locales/pt-BR.ts +0 -242
  36. package/src/migrations/000_plg_rename.sql +0 -27
  37. package/src/migrations/001_inventory_base.sql +0 -69
  38. package/src/migrations/002_recipes.sql +0 -34
  39. package/src/migrations/003_measurement_units.sql +0 -13
  40. package/src/migrations/index.ts +0 -161
  41. package/src/registries.ts +0 -111
  42. package/src/store.ts +0 -127
  43. package/src/types.ts +0 -256
  44. package/src/views/DashboardView.tsx +0 -11
  45. package/src/views/MovementHistoryView.tsx +0 -104
  46. package/src/views/ProductCrudForm.tsx +0 -104
  47. package/src/views/ProductListView.tsx +0 -58
  48. package/src/views/RecipeDetailView.tsx +0 -192
  49. package/src/views/RecipeFormView.tsx +0 -241
  50. package/src/views/RecipesView.tsx +0 -106
  51. package/src/views/StockMovementView.tsx +0 -518
  52. package/src/views/dashboardWidgets.tsx +0 -101
  53. package/src/views/productEntity.tsx +0 -124
  54. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
package/src/data/mock.ts DELETED
@@ -1,266 +0,0 @@
1
- import type { InventoryDataProvider } from './types'
2
- import type {
3
- Product, StockMovement, StockPosition, StockLocation,
4
- Recipe, RecipeIngredient,
5
- CreateProductInput, CreateStockMovementInput, CreateRecipeInput,
6
- ProductQuery, MovementQuery,
7
- PaginatedResult, InventorySummary, MovementType,
8
- } from '../types'
9
-
10
- let nextId = 1
11
- function uid(): string { return String(nextId++) }
12
- function now(): string { return new Date().toISOString() }
13
- function today(): string { return new Date().toISOString().slice(0, 10) }
14
-
15
- function paginate<T>(items: T[], page?: number, pageSize?: number): PaginatedResult<T> {
16
- const p = page ?? 1
17
- const ps = pageSize ?? 50
18
- const start = (p - 1) * ps
19
- return { data: items.slice(start, start + ps), total: items.length }
20
- }
21
-
22
- interface MockStore {
23
- products: Product[]
24
- movements: StockMovement[]
25
- positions: StockPosition[]
26
- locations: StockLocation[]
27
- recipes: Recipe[]
28
- recipeIngredients: RecipeIngredient[]
29
- }
30
-
31
- function createStore(): MockStore {
32
- return {
33
- products: [],
34
- movements: [],
35
- positions: [],
36
- locations: [
37
- { id: uid(), name: 'Main Storage', description: 'Primary storage area', isActive: true, tenantId: 'mock-tenant', createdAt: now(), updatedAt: now() },
38
- ],
39
- recipes: [],
40
- recipeIngredients: [],
41
- }
42
- }
43
-
44
- export function createMockInventoryProvider(): InventoryDataProvider {
45
- const store = createStore()
46
- const tenantId = 'mock-tenant'
47
-
48
- const provider: InventoryDataProvider = {
49
- // --- Products ---
50
- async getProducts(query: ProductQuery): Promise<PaginatedResult<Product>> {
51
- let results = [...store.products]
52
- if (query.productType) results = results.filter((p) => p.productType === query.productType)
53
- if (query.categoryId) results = results.filter((p) => p.categoryId === query.categoryId)
54
- if (query.isActive !== undefined) results = results.filter((p) => p.isActive === query.isActive)
55
- if (query.lowStockOnly) results = results.filter((p) => p.currentQuantity <= p.minQuantity)
56
- if (query.search) {
57
- const s = query.search.toLowerCase()
58
- results = results.filter((p) => p.name.toLowerCase().includes(s) || p.sku?.toLowerCase().includes(s) || p.barcode?.includes(s))
59
- }
60
- results.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
61
- return paginate(results, query.page, query.pageSize)
62
- },
63
-
64
- async getProductById(id: string): Promise<Product | null> {
65
- return store.products.find((p) => p.id === id) ?? null
66
- },
67
-
68
- async createProduct(input: CreateProductInput): Promise<Product> {
69
- const product: Product = {
70
- id: uid(),
71
- name: input.name,
72
- description: input.description,
73
- sku: input.sku,
74
- barcode: input.barcode,
75
- brand: input.brand,
76
- categoryId: input.categoryId,
77
- productType: input.productType,
78
- purpose: input.purpose,
79
- currentQuantity: 0,
80
- minQuantity: input.minQuantity ?? 0,
81
- maxQuantity: input.maxQuantity,
82
- costPrice: input.costPrice ?? 0,
83
- salePrice: input.salePrice,
84
- measurementUnitId: input.measurementUnitId,
85
- isActive: true,
86
- supplierId: input.supplierId,
87
- defaultLocationId: input.defaultLocationId,
88
- imageUrl: input.imageUrl,
89
- metadata: input.metadata,
90
- tenantId,
91
- createdAt: now(),
92
- updatedAt: now(),
93
- }
94
- store.products.push(product)
95
- return product
96
- },
97
-
98
- async updateProduct(id: string, data: Partial<Product>): Promise<Product> {
99
- const product = store.products.find((p) => p.id === id)
100
- if (!product) throw new Error(`Product ${id} not found`)
101
- Object.assign(product, data, { updatedAt: now() })
102
- return product
103
- },
104
-
105
- // --- Stock Movements ---
106
- async getMovements(query: MovementQuery): Promise<PaginatedResult<StockMovement>> {
107
- let results = [...store.movements]
108
- if (query.productId) results = results.filter((m) => m.productId === query.productId)
109
- if (query.movementType) {
110
- const types = Array.isArray(query.movementType) ? query.movementType : [query.movementType]
111
- results = results.filter((m) => types.includes(m.movementType))
112
- }
113
- if (query.stockLocationId) results = results.filter((m) => m.stockLocationId === query.stockLocationId)
114
- if (query.dateRange) {
115
- results = results.filter((m) => m.movementDate >= query.dateRange!.from && m.movementDate <= query.dateRange!.to)
116
- }
117
- if (query.search) {
118
- const s = query.search.toLowerCase()
119
- results = results.filter((m) => m.productName?.toLowerCase().includes(s) || m.notes?.toLowerCase().includes(s))
120
- }
121
- results.sort((a, b) => b.movementDate.localeCompare(a.movementDate))
122
- return paginate(results, query.page, query.pageSize)
123
- },
124
-
125
- async createMovement(input: CreateStockMovementInput): Promise<StockMovement> {
126
- const product = store.products.find((p) => p.id === input.productId)
127
- const location = input.stockLocationId ? store.locations.find((l) => l.id === input.stockLocationId) : undefined
128
- const unitCost = input.unitCost ?? product?.costPrice ?? 0
129
-
130
- const movement: StockMovement = {
131
- id: uid(),
132
- productId: input.productId,
133
- productName: product?.name,
134
- quantity: input.quantity,
135
- movementType: input.movementType,
136
- unitCost,
137
- totalCost: unitCost * input.quantity,
138
- stockLocationId: input.stockLocationId,
139
- stockLocationName: location?.name,
140
- destinationLocationId: input.destinationLocationId,
141
- supplierId: input.supplierId,
142
- documentNumber: input.documentNumber,
143
- reason: input.reason,
144
- notes: input.notes,
145
- movementDate: input.movementDate ?? today(),
146
- tenantId,
147
- createdAt: now(),
148
- }
149
- store.movements.push(movement)
150
-
151
- // Update product quantity
152
- if (product) {
153
- if (input.movementType === 'entry') product.currentQuantity += input.quantity
154
- else if (input.movementType === 'exit' || input.movementType === 'loss') product.currentQuantity -= input.quantity
155
- // adjustment sets absolute, transfer moves between locations
156
- product.updatedAt = now()
157
- }
158
-
159
- return movement
160
- },
161
-
162
- // --- Stock Positions ---
163
- async getPositions(productId: string): Promise<StockPosition[]> {
164
- return store.positions.filter((p) => p.productId === productId)
165
- },
166
-
167
- // --- Stock Locations ---
168
- async getLocations(): Promise<StockLocation[]> {
169
- return store.locations.filter((l) => l.isActive)
170
- },
171
-
172
- async createLocation(data): Promise<StockLocation> {
173
- const location: StockLocation = {
174
- id: uid(),
175
- name: data.name,
176
- description: data.description,
177
- isActive: true,
178
- unitId: data.unitId,
179
- tenantId,
180
- createdAt: now(),
181
- updatedAt: now(),
182
- }
183
- store.locations.push(location)
184
- return location
185
- },
186
-
187
- // --- Recipes ---
188
- async getRecipes(): Promise<Recipe[]> {
189
- return store.recipes.filter((r) => r.isActive)
190
- },
191
-
192
- async getRecipeById(id: string): Promise<Recipe | null> {
193
- return store.recipes.find((r) => r.id === id) ?? null
194
- },
195
-
196
- async getRecipeIngredients(recipeId: string): Promise<RecipeIngredient[]> {
197
- return store.recipeIngredients.filter((ri) => ri.recipeId === recipeId).sort((a, b) => a.displayOrder - b.displayOrder)
198
- },
199
-
200
- async createRecipe(input: CreateRecipeInput): Promise<Recipe> {
201
- const product = store.products.find((p) => p.id === input.productId)
202
- const recipeId = uid()
203
- const recipe: Recipe = {
204
- id: recipeId,
205
- name: input.name,
206
- description: input.description,
207
- productId: input.productId,
208
- productName: product?.name,
209
- yieldQuantity: input.yieldQuantity,
210
- yieldUnitId: input.yieldUnitId,
211
- preparationTimeMinutes: input.preparationTimeMinutes,
212
- instructions: input.instructions,
213
- isActive: true,
214
- ingredientCount: input.ingredients.length,
215
- tenantId,
216
- createdAt: now(),
217
- updatedAt: now(),
218
- }
219
- store.recipes.push(recipe)
220
-
221
- for (const ing of input.ingredients) {
222
- const ingProduct = store.products.find((p) => p.id === ing.productId)
223
- store.recipeIngredients.push({
224
- id: uid(),
225
- recipeId,
226
- productId: ing.productId,
227
- productName: ingProduct?.name,
228
- quantity: ing.quantity,
229
- unitId: ing.unitId,
230
- displayOrder: ing.displayOrder ?? 0,
231
- notes: ing.notes,
232
- createdAt: now(),
233
- })
234
- }
235
-
236
- return recipe
237
- },
238
-
239
- // --- Summary ---
240
- async getSummary(): Promise<InventorySummary> {
241
- const active = store.products.filter((p) => p.isActive)
242
- const lowStock = active.filter((p) => p.currentQuantity <= p.minQuantity && p.currentQuantity > 0)
243
- const outOfStock = active.filter((p) => p.currentQuantity <= 0)
244
- const totalValue = active.reduce((sum, p) => sum + p.currentQuantity * p.costPrice, 0)
245
-
246
- const weekAgo = new Date()
247
- weekAgo.setDate(weekAgo.getDate() - 7)
248
- const weekAgoStr = weekAgo.toISOString().slice(0, 10)
249
- const recent = store.movements.filter((m) => m.movementDate >= weekAgoStr)
250
-
251
- const movementsByType: Record<MovementType, number> = { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 }
252
- for (const m of recent) movementsByType[m.movementType]++
253
-
254
- return {
255
- totalProducts: active.length,
256
- lowStockCount: lowStock.length,
257
- outOfStockCount: outOfStock.length,
258
- totalStockValue: totalValue,
259
- recentMovementCount: recent.length,
260
- movementsByType,
261
- }
262
- },
263
- }
264
-
265
- return provider
266
- }
@@ -1,361 +0,0 @@
1
- import type { InventoryDataProvider } from './types'
2
- import type {
3
- Product, StockMovement, StockPosition, StockLocation,
4
- Recipe, RecipeIngredient,
5
- CreateProductInput, CreateStockMovementInput, CreateRecipeInput,
6
- ProductQuery, MovementQuery,
7
- PaginatedResult, InventorySummary, MovementType,
8
- } from '../types'
9
- import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
10
- import { getInventoryTenantId } from '../lib/tenant'
11
- import { T } from './tables'
12
-
13
- function getTenantId(): string | undefined {
14
- // Local override wins; else use the app's active tenant so writes pass RLS.
15
- return getInventoryTenantId() ?? getActiveTenantId()
16
- }
17
-
18
- function snakeToCamel(obj: Record<string, unknown>): Record<string, unknown> {
19
- const result: Record<string, unknown> = {}
20
- for (const [key, value] of Object.entries(obj)) {
21
- const camelKey = key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
22
- result[camelKey] = value
23
- }
24
- return result
25
- }
26
-
27
- function camelToSnake(obj: Record<string, unknown>): Record<string, unknown> {
28
- const result: Record<string, unknown> = {}
29
- for (const [key, value] of Object.entries(obj)) {
30
- if (key.startsWith('_')) continue
31
- const snakeKey = key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)
32
- result[snakeKey] = value
33
- }
34
- return result
35
- }
36
-
37
- /** Map a public.products row to our Product type */
38
- function mapProductRow(row: Record<string, any>): Product {
39
- const meta = row.metadata ?? {}
40
- return {
41
- id: row.id,
42
- name: row.name,
43
- description: row.description,
44
- sku: row.sku,
45
- barcode: meta.barcode,
46
- brand: meta.brand,
47
- productType: meta.productType ?? 'sale',
48
- currentQuantity: row.stock ?? 0,
49
- minQuantity: row.min_stock ?? 0,
50
- maxQuantity: meta.maxQuantity,
51
- costPrice: row.cost ?? 0,
52
- salePrice: row.price,
53
- isActive: row.is_active ?? true,
54
- imageUrl: row.image_url,
55
- metadata: meta,
56
- tenantId: row.tenant_id,
57
- createdAt: row.created_at,
58
- updatedAt: row.updated_at,
59
- }
60
- }
61
-
62
- export function createSupabaseInventoryProvider(): InventoryDataProvider {
63
- // Lazy resolution — Supabase client may not exist at factory time
64
- // but will be available when methods are called (after createSaasApp initializes it)
65
- function getClients() {
66
- const supabase = getSupabaseClientOptional() as any
67
- if (!supabase) throw new Error('Supabase not initialized')
68
- return { core: supabase, pub: supabase }
69
- }
70
-
71
- const provider: InventoryDataProvider = {
72
- // --- Products (public.products) ---
73
- async getProducts(query: ProductQuery): Promise<PaginatedResult<Product>> {
74
- const { core, pub } = getClients()
75
- let qb = core.from('products').select('*', { count: 'exact' })
76
- if (query.search) qb = qb.ilike('name', `%${query.search}%`)
77
- // productType lives in the metadata JSON column → filter on metadata->>productType.
78
- if (query.productType) qb = qb.eq('metadata->>productType', query.productType)
79
- if (query.isActive !== undefined) qb = qb.eq('is_active', query.isActive)
80
- if (query.lowStockOnly) qb = qb.lte('stock', 0) // simplified
81
- const page = query.page ?? 1
82
- const pageSize = query.pageSize ?? 50
83
- qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order('created_at', { ascending: false })
84
- const { data, count } = await qb
85
- return { data: (data ?? []).map(mapProductRow), total: count ?? 0 }
86
- },
87
-
88
- async getProductById(id: string): Promise<Product | null> {
89
- const { core } = getClients()
90
- const { data } = await core.from('products').select('*').eq('id', id).single()
91
- return data ? mapProductRow(data) : null
92
- },
93
-
94
- async createProduct(input: CreateProductInput): Promise<Product> {
95
- const { core } = getClients()
96
- const tenantId = getTenantId()
97
- const row: Record<string, unknown> = {
98
- tenant_id: tenantId,
99
- name: input.name,
100
- description: input.description,
101
- sku: input.sku,
102
- price: input.salePrice ?? 0,
103
- cost: input.costPrice ?? 0,
104
- stock: 0,
105
- min_stock: input.minQuantity ?? 0,
106
- is_active: true,
107
- image_url: input.imageUrl,
108
- metadata: { productType: input.productType, barcode: input.barcode, brand: input.brand, maxQuantity: input.maxQuantity },
109
- }
110
- const { data, error } = await core.from('products').insert(row).select().single()
111
- if (error) throw new Error(error.message)
112
- return mapProductRow(data!)
113
- },
114
-
115
- async updateProduct(id: string, partial: Partial<Product>): Promise<Product> {
116
- const { core } = getClients()
117
- const row: Record<string, unknown> = {}
118
- if (partial.name !== undefined) row.name = partial.name
119
- if (partial.description !== undefined) row.description = partial.description
120
- if (partial.sku !== undefined) row.sku = partial.sku
121
- if (partial.salePrice !== undefined) row.price = partial.salePrice
122
- if (partial.costPrice !== undefined) row.cost = partial.costPrice
123
- if (partial.minQuantity !== undefined) row.min_stock = partial.minQuantity
124
- if (partial.isActive !== undefined) row.is_active = partial.isActive
125
- if (partial.imageUrl !== undefined) row.image_url = partial.imageUrl
126
- if (partial.productType !== undefined || partial.barcode !== undefined || partial.brand !== undefined) {
127
- row.metadata = { productType: partial.productType, barcode: partial.barcode, brand: partial.brand }
128
- }
129
- const { data, error } = await core.from('products').update(row).eq('id', id).select().single()
130
- if (error) throw new Error(error.message)
131
- return mapProductRow(data!)
132
- },
133
-
134
- // --- Stock Movements (via view with product join — single query) ---
135
- async getMovements(query: MovementQuery): Promise<PaginatedResult<StockMovement>> {
136
- const { pub } = getClients()
137
- // Single query via v_stock_movements view (JOINs with public.products)
138
- let qb = pub.from('v_stock_movements').select('*', { count: 'exact' })
139
- if (query.productId) qb = qb.eq('product_id', query.productId)
140
- if (query.movementType) {
141
- const types = Array.isArray(query.movementType) ? query.movementType : [query.movementType]
142
- qb = qb.in('movement_type', types)
143
- }
144
- if (query.stockLocationId) qb = qb.eq('stock_location_id', query.stockLocationId)
145
- if (query.dateRange) qb = qb.gte('movement_date', query.dateRange.from).lte('movement_date', query.dateRange.to)
146
- if (query.search) qb = qb.ilike('product_name', `%${query.search}%`)
147
- const page = query.page ?? 1
148
- const pageSize = query.pageSize ?? 50
149
- qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order('movement_date', { ascending: false })
150
- const { data, count } = await qb
151
-
152
- const movements = (data ?? []).map((r: any) => {
153
- const mov = snakeToCamel(r) as any
154
- mov.productName = r.product_name ?? mov.productName
155
- mov.productSku = r.product_sku ?? mov.productSku
156
- // View may include location names if the updated migration has been applied
157
- mov.stockLocationName = r.stock_location_name ?? mov.stockLocationName
158
- mov.destinationLocationName = r.destination_location_name ?? mov.destinationLocationName
159
- return mov as StockMovement
160
- })
161
-
162
- // Resolve location names if not provided by the view
163
- const needsLocationResolve = movements.some(
164
- (m: any) => (m.stockLocationId && !m.stockLocationName) || (m.destinationLocationId && !m.destinationLocationName)
165
- )
166
- if (needsLocationResolve) {
167
- const locationIds = new Set<string>()
168
- for (const m of movements) {
169
- if (m.stockLocationId && !m.stockLocationName) locationIds.add(m.stockLocationId)
170
- if (m.destinationLocationId && !m.destinationLocationName) locationIds.add(m.destinationLocationId)
171
- }
172
- if (locationIds.size > 0) {
173
- const { data: locs } = await pub.from(T.stockLocations).select('id, name').in('id', [...locationIds])
174
- const locMap = new Map((locs ?? []).map((l: any) => [l.id, l.name]))
175
- for (const m of movements) {
176
- if (m.stockLocationId && !m.stockLocationName) m.stockLocationName = locMap.get(m.stockLocationId)
177
- if (m.destinationLocationId && !m.destinationLocationName) m.destinationLocationName = locMap.get(m.destinationLocationId)
178
- }
179
- }
180
- }
181
-
182
- return { data: movements, total: count ?? 0 }
183
- },
184
-
185
- async createMovement(input: CreateStockMovementInput): Promise<StockMovement> {
186
- const { core, pub } = getClients()
187
- const tenantId = getTenantId()
188
- const unitCost = input.unitCost ?? 0
189
- const row = {
190
- ...camelToSnake(input as any),
191
- tenant_id: tenantId,
192
- unit_cost: unitCost,
193
- total_cost: unitCost * input.quantity,
194
- movement_date: input.movementDate ?? new Date().toISOString().slice(0, 10),
195
- }
196
- const { data, error } = await pub.from(T.stockMovements).insert(row).select().single()
197
- if (error) throw new Error(error.message)
198
-
199
- // Fetch product name + current stock for the response and stock update
200
- const { data: product } = await core.from('products').select('id, name, stock').eq('id', input.productId).single()
201
-
202
- // Update product stock
203
- if (product) {
204
- const delta = (input.movementType === 'entry') ? input.quantity : -(input.quantity)
205
- if (input.movementType !== 'adjustment' && input.movementType !== 'transfer') {
206
- await core.from('products').update({ stock: (product.stock ?? 0) + delta }).eq('id', input.productId)
207
- }
208
- }
209
-
210
- // Update stock_positions when a location is specified (best-effort — don't fail the movement)
211
- if (input.stockLocationId) {
212
- try {
213
- const posDelta = (input.movementType === 'entry') ? input.quantity : -(input.quantity)
214
- const { data: existing } = await pub.from(T.stockPositions)
215
- .select('id, quantity')
216
- .eq('product_id', input.productId)
217
- .eq('stock_location_id', input.stockLocationId)
218
- .maybeSingle()
219
-
220
- if (existing) {
221
- await pub.from(T.stockPositions)
222
- .update({ quantity: (existing.quantity ?? 0) + posDelta, unit_cost: input.unitCost ?? 0 })
223
- .eq('id', existing.id)
224
- } else {
225
- await pub.from(T.stockPositions).insert({
226
- tenant_id: tenantId,
227
- product_id: input.productId,
228
- stock_location_id: input.stockLocationId,
229
- quantity: Math.max(0, posDelta),
230
- unit_cost: input.unitCost ?? 0,
231
- batch_number: input.batchNumber ?? null,
232
- expiration_date: input.expirationDate ?? null,
233
- })
234
- }
235
-
236
- // For transfers, also update the destination position
237
- if (input.movementType === 'transfer' && input.destinationLocationId) {
238
- const { data: destExisting } = await pub.from(T.stockPositions)
239
- .select('id, quantity')
240
- .eq('product_id', input.productId)
241
- .eq('stock_location_id', input.destinationLocationId)
242
- .maybeSingle()
243
-
244
- if (destExisting) {
245
- await pub.from(T.stockPositions)
246
- .update({ quantity: (destExisting.quantity ?? 0) + input.quantity, unit_cost: input.unitCost ?? 0 })
247
- .eq('id', destExisting.id)
248
- } else {
249
- await pub.from(T.stockPositions).insert({
250
- tenant_id: tenantId,
251
- product_id: input.productId,
252
- stock_location_id: input.destinationLocationId,
253
- quantity: input.quantity,
254
- unit_cost: input.unitCost ?? 0,
255
- batch_number: input.batchNumber ?? null,
256
- expiration_date: input.expirationDate ?? null,
257
- })
258
- }
259
- }
260
- } catch {
261
- // stock_positions update is best-effort — log but don't fail the movement
262
- console.warn('Failed to update stock_positions — table may not exist yet')
263
- }
264
- }
265
-
266
- const movement = snakeToCamel(data) as any
267
- movement.productName = product?.name
268
- // Resolve location name
269
- if (input.stockLocationId) {
270
- const { data: loc } = await pub.from(T.stockLocations).select('name').eq('id', input.stockLocationId).single()
271
- movement.stockLocationName = loc?.name
272
- }
273
- if (input.destinationLocationId) {
274
- const { data: loc } = await pub.from(T.stockLocations).select('name').eq('id', input.destinationLocationId).single()
275
- movement.destinationLocationName = loc?.name
276
- }
277
- return movement as StockMovement
278
- },
279
-
280
- // --- Stock Positions ---
281
- async getPositions(productId: string): Promise<StockPosition[]> {
282
- const { pub } = getClients()
283
- const { data } = await pub.from(T.stockPositions).select('*').eq('product_id', productId)
284
- return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as StockPosition)
285
- },
286
-
287
- // --- Stock Locations ---
288
- async getLocations(): Promise<StockLocation[]> {
289
- const { pub } = getClients()
290
- const { data } = await pub.from(T.stockLocations).select('*').eq('is_active', true).order('name')
291
- return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as StockLocation)
292
- },
293
-
294
- async createLocation(input): Promise<StockLocation> {
295
- const { pub } = getClients()
296
- const tenantId = getTenantId()
297
- const { data } = await pub.from(T.stockLocations).insert({ ...camelToSnake(input as any), tenant_id: tenantId }).select().single()
298
- return snakeToCamel(data!) as unknown as StockLocation
299
- },
300
-
301
- // --- Recipes ---
302
- async getRecipes(): Promise<Recipe[]> {
303
- const { pub } = getClients()
304
- const { data } = await pub.from(T.recipes).select('*').eq('is_active', true).order('name')
305
- return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as Recipe)
306
- },
307
-
308
- async getRecipeById(id: string): Promise<Recipe | null> {
309
- const { pub } = getClients()
310
- const { data } = await pub.from(T.recipes).select('*').eq('id', id).single()
311
- return data ? snakeToCamel(data) as unknown as Recipe : null
312
- },
313
-
314
- async getRecipeIngredients(recipeId: string): Promise<RecipeIngredient[]> {
315
- const { pub } = getClients()
316
- const { data } = await pub.from(T.recipeIngredients).select('*').eq('recipe_id', recipeId).order('display_order')
317
- return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as RecipeIngredient)
318
- },
319
-
320
- async createRecipe(input: CreateRecipeInput): Promise<Recipe> {
321
- const { pub } = getClients()
322
- const tenantId = getTenantId()
323
- const { ingredients, ...recipeData } = input
324
- const { data: recipe } = await pub.from(T.recipes).insert({ ...camelToSnake(recipeData as any), tenant_id: tenantId }).select().single()
325
- if (recipe && ingredients.length > 0) {
326
- await pub.from(T.recipeIngredients).insert(
327
- ingredients.map((ing) => ({ ...camelToSnake(ing as any), recipe_id: recipe.id, tenant_id: tenantId }))
328
- )
329
- }
330
- return snakeToCamel(recipe!) as unknown as Recipe
331
- },
332
-
333
- // --- Summary ---
334
- async getSummary(): Promise<InventorySummary> {
335
- const { core, pub } = getClients()
336
- const { data: products } = await core.from('products').select('stock, min_stock, price, is_active').eq('is_active', true)
337
- const items = products ?? []
338
- const lowStock = items.filter((p: any) => p.stock > 0 && p.stock <= (p.min_stock ?? 0))
339
- const outOfStock = items.filter((p: any) => p.stock <= 0)
340
- const totalValue = items.reduce((sum: number, p: any) => sum + (p.stock ?? 0) * (p.price ?? 0), 0)
341
-
342
- const weekAgo = new Date()
343
- weekAgo.setDate(weekAgo.getDate() - 7)
344
- const { data: movements } = await pub.from(T.stockMovements).select('movement_type').gte('movement_date', weekAgo.toISOString().slice(0, 10))
345
- const movs = movements ?? []
346
- const movementsByType: Record<MovementType, number> = { entry: 0, exit: 0, adjustment: 0, transfer: 0, loss: 0 }
347
- for (const m of movs) movementsByType[m.movement_type as MovementType]++
348
-
349
- return {
350
- totalProducts: items.length,
351
- lowStockCount: lowStock.length,
352
- outOfStockCount: outOfStock.length,
353
- totalStockValue: totalValue,
354
- recentMovementCount: movs.length,
355
- movementsByType,
356
- }
357
- },
358
- }
359
-
360
- return provider
361
- }
@@ -1,10 +0,0 @@
1
- // Central physical-table-name registry for plugin-inventory.
2
- export const T = {
3
- stockLocations: 'plg_inventory_stock_locations',
4
- stockMovements: 'plg_inventory_stock_movements',
5
- stockPositions: 'plg_inventory_stock_positions',
6
- recipes: 'plg_inventory_recipes',
7
- recipeIngredients: 'plg_inventory_recipe_ingredients',
8
- measurementUnits: 'plg_inventory_measurement_units',
9
- productCategories: 'plg_inventory_product_categories',
10
- } as const
package/src/data/types.ts DELETED
@@ -1,35 +0,0 @@
1
- import type {
2
- Product, StockMovement, StockPosition, StockLocation,
3
- Recipe, RecipeIngredient,
4
- CreateProductInput, CreateStockMovementInput, CreateRecipeInput,
5
- ProductQuery, MovementQuery,
6
- PaginatedResult, InventorySummary,
7
- } from '../types'
8
-
9
- export interface InventoryDataProvider {
10
- // --- Products ---
11
- getProducts(query: ProductQuery): Promise<PaginatedResult<Product>>
12
- getProductById(id: string): Promise<Product | null>
13
- createProduct(input: CreateProductInput): Promise<Product>
14
- updateProduct(id: string, data: Partial<Product>): Promise<Product>
15
-
16
- // --- Stock Movements ---
17
- getMovements(query: MovementQuery): Promise<PaginatedResult<StockMovement>>
18
- createMovement(input: CreateStockMovementInput): Promise<StockMovement>
19
-
20
- // --- Stock Positions ---
21
- getPositions(productId: string): Promise<StockPosition[]>
22
-
23
- // --- Stock Locations ---
24
- getLocations(): Promise<StockLocation[]>
25
- createLocation(data: { name: string; description?: string; unitId?: string }): Promise<StockLocation>
26
-
27
- // --- Recipes ---
28
- getRecipes(): Promise<Recipe[]>
29
- getRecipeById(id: string): Promise<Recipe | null>
30
- getRecipeIngredients(recipeId: string): Promise<RecipeIngredient[]>
31
- createRecipe(input: CreateRecipeInput): Promise<Recipe>
32
-
33
- // --- Summary ---
34
- getSummary(): Promise<InventorySummary>
35
- }