@fayz-ai/plugin-orders 0.2.0
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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/OrdersContext.d.ts +40 -0
- package/dist/OrdersContext.d.ts.map +1 -0
- package/dist/OrdersPage-2IXARHDR.cjs +157 -0
- package/dist/OrdersPage-2IXARHDR.cjs.map +1 -0
- package/dist/OrdersPage-LJIEIZYE.js +155 -0
- package/dist/OrdersPage-LJIEIZYE.js.map +1 -0
- package/dist/OrdersPage.d.ts +13 -0
- package/dist/OrdersPage.d.ts.map +1 -0
- package/dist/chunk-7LFVFZKZ.js +23 -0
- package/dist/chunk-7LFVFZKZ.js.map +1 -0
- package/dist/chunk-WBGECNYU.cjs +28 -0
- package/dist/chunk-WBGECNYU.cjs.map +1 -0
- package/dist/data/fayz.d.ts +18 -0
- package/dist/data/fayz.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/types.d.ts +17 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +1185 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1178 -0
- package/dist/index.js.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 +29 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/OrderKanbanView.d.ts +3 -0
- package/dist/views/OrderKanbanView.d.ts.map +1 -0
- package/package.json +54 -0
- package/src/OrdersContext.tsx +47 -0
- package/src/OrdersPage.tsx +20 -0
- package/src/data/fayz.ts +456 -0
- package/src/data/mock.ts +512 -0
- package/src/data/types.ts +23 -0
- package/src/index.ts +217 -0
- package/src/locales/en.ts +53 -0
- package/src/locales/index.ts +4 -0
- package/src/locales/pt-BR.ts +53 -0
- package/src/registries.ts +32 -0
- package/src/store.ts +191 -0
- package/src/types.ts +190 -0
- package/src/views/OrderKanbanView.tsx +240 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
+
import type { EntityLookup } from '@fayz-ai/saas'
|
|
4
|
+
import type { OrdersDataProvider } from './data/types'
|
|
5
|
+
import type { RestaurantOrder } from './types'
|
|
6
|
+
import { createMockOrdersProvider } from './data/mock'
|
|
7
|
+
import { createOrdersStore } from './store'
|
|
8
|
+
import { ordersRegistries } from './registries'
|
|
9
|
+
import { ordersLocales } from './locales'
|
|
10
|
+
import { PluginSettingsPanel } from '@fayz-ai/saas'
|
|
11
|
+
|
|
12
|
+
const OrdersPage = React.lazy(() => import('./OrdersPage').then((m) => ({ default: m.OrdersPage })))
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Labels
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export interface OrdersPluginLabels {
|
|
19
|
+
pageTitle: string
|
|
20
|
+
kanban: string
|
|
21
|
+
newOrder: string
|
|
22
|
+
orderList: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_LABELS: OrdersPluginLabels = {
|
|
26
|
+
pageTitle: 'Orders',
|
|
27
|
+
kanban: 'Orders',
|
|
28
|
+
newOrder: 'New Order',
|
|
29
|
+
orderList: 'History',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Currency + sources
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const DEFAULT_CURRENCY = { code: 'BRL', locale: 'pt-BR', symbol: 'R$' }
|
|
37
|
+
|
|
38
|
+
export interface OrderSourceOption {
|
|
39
|
+
value: string
|
|
40
|
+
label: string
|
|
41
|
+
icon?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DEFAULT_ORDER_SOURCES: OrderSourceOption[] = [
|
|
45
|
+
{ value: 'dine_in', label: 'Dine-in', icon: '🍽️' },
|
|
46
|
+
{ value: 'takeout', label: 'Takeout', icon: '🥡' },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Options
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
export interface OrdersPluginOptions {
|
|
54
|
+
modules?: {
|
|
55
|
+
delivery?: boolean
|
|
56
|
+
takeout?: boolean
|
|
57
|
+
}
|
|
58
|
+
labels?: Partial<OrdersPluginLabels>
|
|
59
|
+
currency?: { code?: string; locale?: string; symbol?: string }
|
|
60
|
+
navPosition?: number
|
|
61
|
+
navSection?: 'main' | 'secondary'
|
|
62
|
+
scope?: PluginScope
|
|
63
|
+
verticalId?: VerticalId
|
|
64
|
+
dataProvider?: OrdersDataProvider
|
|
65
|
+
menuItemLookup?: EntityLookup
|
|
66
|
+
staffLookup?: EntityLookup
|
|
67
|
+
onOrderCompleted?: (order: RestaurantOrder) => Promise<void>
|
|
68
|
+
orderSources?: OrderSourceOption[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Config resolver
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function resolveConfig(options?: OrdersPluginOptions) {
|
|
76
|
+
return {
|
|
77
|
+
modules: {
|
|
78
|
+
delivery: options?.modules?.delivery !== false,
|
|
79
|
+
takeout: options?.modules?.takeout !== false,
|
|
80
|
+
},
|
|
81
|
+
labels: { ...DEFAULT_LABELS, ...options?.labels } as any,
|
|
82
|
+
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
83
|
+
orderSources: options?.orderSources ?? DEFAULT_ORDER_SOURCES,
|
|
84
|
+
menuItemLookup: options?.menuItemLookup,
|
|
85
|
+
staffLookup: options?.staffLookup,
|
|
86
|
+
onOrderCompleted: options?.onOrderCompleted,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Factory
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export function createOrdersPlugin(options?: OrdersPluginOptions): PluginManifest {
|
|
95
|
+
const config = resolveConfig(options)
|
|
96
|
+
const provider = options?.dataProvider ?? createMockOrdersProvider()
|
|
97
|
+
const store = createOrdersStore(provider)
|
|
98
|
+
|
|
99
|
+
const PageComponent: React.FC<any> = () =>
|
|
100
|
+
React.createElement(React.Suspense, { fallback: null },
|
|
101
|
+
React.createElement(OrdersPage, { config, provider, store, registries: ordersRegistries })
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
id: 'orders',
|
|
106
|
+
name: config.labels.pageTitle,
|
|
107
|
+
icon: 'ClipboardList',
|
|
108
|
+
version: '1.0.0',
|
|
109
|
+
scope: options?.scope ?? 'vertical',
|
|
110
|
+
verticalId: options?.verticalId,
|
|
111
|
+
defaultEnabled: true,
|
|
112
|
+
dependencies: ['menu'],
|
|
113
|
+
navigation: [
|
|
114
|
+
{
|
|
115
|
+
section: options?.navSection ?? 'main',
|
|
116
|
+
position: options?.navPosition ?? 2,
|
|
117
|
+
label: config.labels.pageTitle,
|
|
118
|
+
route: '/orders',
|
|
119
|
+
icon: 'ClipboardList',
|
|
120
|
+
permission: { feature: 'orders', action: 'read' as const },
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
routes: [
|
|
124
|
+
{
|
|
125
|
+
path: '/orders',
|
|
126
|
+
component: PageComponent,
|
|
127
|
+
permission: { feature: 'orders', action: 'read' as const },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
widgets: [],
|
|
131
|
+
aiTools: [
|
|
132
|
+
{
|
|
133
|
+
id: 'orders.today-summary',
|
|
134
|
+
name: 'getOrdersSummary',
|
|
135
|
+
description: 'Returns order count, revenue, and average ticket for today or a given period.',
|
|
136
|
+
icon: 'ClipboardList',
|
|
137
|
+
mode: 'read' as const,
|
|
138
|
+
category: 'Orders',
|
|
139
|
+
parameters: {
|
|
140
|
+
type: 'object' as const,
|
|
141
|
+
properties: {
|
|
142
|
+
period: { type: 'string' as const, description: 'today, week, month' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
suggestions: [
|
|
146
|
+
{ label: 'How many orders today?' },
|
|
147
|
+
{ label: "What's the average ticket today?" },
|
|
148
|
+
{ label: "What's the most ordered item?" },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'orders.active-orders',
|
|
153
|
+
name: 'getActiveOrders',
|
|
154
|
+
description: 'Lists currently active (non-completed) orders.',
|
|
155
|
+
icon: 'Clock',
|
|
156
|
+
mode: 'read' as const,
|
|
157
|
+
category: 'Orders',
|
|
158
|
+
parameters: {
|
|
159
|
+
type: 'object' as const,
|
|
160
|
+
properties: {
|
|
161
|
+
kind: { type: 'string' as const, enum: ['dine_in', 'takeout', 'delivery'] },
|
|
162
|
+
status: { type: 'string' as const, description: 'Filter by status' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
suggestions: [
|
|
166
|
+
{ label: 'What orders are being prepared right now?' },
|
|
167
|
+
{ label: 'Any delivery orders waiting?' },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'orders.create-order',
|
|
172
|
+
name: 'createOrder',
|
|
173
|
+
description: 'Creates a new restaurant order with items.',
|
|
174
|
+
icon: 'PlusCircle',
|
|
175
|
+
mode: 'persist' as const,
|
|
176
|
+
category: 'Orders',
|
|
177
|
+
parameters: {
|
|
178
|
+
type: 'object' as const,
|
|
179
|
+
properties: {
|
|
180
|
+
kind: { type: 'string' as const, enum: ['dine_in', 'takeout', 'delivery'] },
|
|
181
|
+
tableNumber: { type: 'number' as const, description: 'Table number (for dine-in)' },
|
|
182
|
+
items: { type: 'array' as const, description: 'Item names and quantities' },
|
|
183
|
+
},
|
|
184
|
+
required: ['kind', 'items'],
|
|
185
|
+
},
|
|
186
|
+
permission: { feature: 'orders', action: 'create' as const },
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
registries: ordersRegistries,
|
|
190
|
+
settings: [
|
|
191
|
+
{
|
|
192
|
+
id: 'orders',
|
|
193
|
+
label: config.labels.pageTitle,
|
|
194
|
+
icon: 'ClipboardList',
|
|
195
|
+
component: (() => {
|
|
196
|
+
const OrdersSettingsTab: React.ComponentType<unknown> = () =>
|
|
197
|
+
React.createElement(PluginSettingsPanel, {
|
|
198
|
+
title: 'Orders Settings',
|
|
199
|
+
subtitle: 'Order sources and preferences',
|
|
200
|
+
registries: ordersRegistries,
|
|
201
|
+
routeBase: '/settings/orders',
|
|
202
|
+
})
|
|
203
|
+
OrdersSettingsTab.displayName = 'OrdersSettingsTab'
|
|
204
|
+
return OrdersSettingsTab
|
|
205
|
+
})(),
|
|
206
|
+
order: 22,
|
|
207
|
+
permission: { feature: 'orders', action: 'read' as const },
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
locales: ordersLocales,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type { OrdersDataProvider } from './data/types'
|
|
215
|
+
export { createFayzOrdersProvider } from './data/fayz'
|
|
216
|
+
export type { FayzOrdersProviderOptions } from './data/fayz'
|
|
217
|
+
export type { ResolvedOrdersConfig } from './OrdersContext'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const en: Record<string, string> = {
|
|
2
|
+
'orders.title': 'Orders',
|
|
3
|
+
'orders.nav.kanban': 'Orders',
|
|
4
|
+
'orders.nav.list': 'History',
|
|
5
|
+
'orders.nav.new': 'New Order',
|
|
6
|
+
'orders.nav.dashboard': 'Dashboard',
|
|
7
|
+
'orders.kanban.title': 'Orders',
|
|
8
|
+
'orders.kanban.subtitle': 'Active orders',
|
|
9
|
+
'orders.kanban.noOrders': 'No active orders',
|
|
10
|
+
'orders.kanban.noOrdersDesc': 'Orders will appear here as they come in',
|
|
11
|
+
'orders.kanban.newOrder': 'New Order',
|
|
12
|
+
'orders.kanban.advance': 'Advance',
|
|
13
|
+
'orders.kanban.cancel': 'Cancel Order',
|
|
14
|
+
'orders.kanban.column.new': 'New',
|
|
15
|
+
'orders.kanban.column.confirmed': 'Confirmed',
|
|
16
|
+
'orders.kanban.column.preparing': 'Preparing',
|
|
17
|
+
'orders.kanban.column.ready': 'Ready',
|
|
18
|
+
'orders.kanban.column.out': 'Out',
|
|
19
|
+
'orders.kanban.column.completed': 'Completed',
|
|
20
|
+
'orders.kanban.column.cancelled': 'Cancelled',
|
|
21
|
+
'orders.detail.title': 'Order Details',
|
|
22
|
+
'orders.detail.items': 'Items',
|
|
23
|
+
'orders.detail.table': 'Table',
|
|
24
|
+
'orders.detail.customer': 'Customer',
|
|
25
|
+
'orders.detail.waiter': 'Waiter',
|
|
26
|
+
'orders.detail.source': 'Source',
|
|
27
|
+
'orders.detail.subtotal': 'Subtotal',
|
|
28
|
+
'orders.detail.discount': 'Discount',
|
|
29
|
+
'orders.detail.tax': 'Tax',
|
|
30
|
+
'orders.detail.total': 'Total',
|
|
31
|
+
'orders.detail.notes': 'Notes',
|
|
32
|
+
'orders.newOrder.title': 'New Order',
|
|
33
|
+
'orders.newOrder.selectKind': 'Order type',
|
|
34
|
+
'orders.newOrder.dineIn': 'Dine-in',
|
|
35
|
+
'orders.newOrder.takeout': 'Takeout',
|
|
36
|
+
'orders.newOrder.delivery': 'Delivery',
|
|
37
|
+
'orders.newOrder.selectTable': 'Select table',
|
|
38
|
+
'orders.newOrder.addItems': 'Add items',
|
|
39
|
+
'orders.newOrder.searchItems': 'Search menu...',
|
|
40
|
+
'orders.newOrder.noItems': 'No items added yet',
|
|
41
|
+
'orders.newOrder.confirm': 'Place Order',
|
|
42
|
+
'orders.newOrder.cancel': 'Cancel',
|
|
43
|
+
'orders.list.title': 'Order History',
|
|
44
|
+
'orders.list.subtitle': 'All orders',
|
|
45
|
+
'orders.list.search': 'Search orders...',
|
|
46
|
+
'orders.list.noOrders': 'No orders found',
|
|
47
|
+
'orders.source.dine_in': 'Dine-in',
|
|
48
|
+
'orders.source.takeout': 'Takeout',
|
|
49
|
+
'orders.source.ifood': 'iFood',
|
|
50
|
+
'orders.source.99food': '99Food',
|
|
51
|
+
'orders.source.delivery': 'Delivery',
|
|
52
|
+
'orders.settings.title': 'Orders Settings',
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const ptBR: Record<string, string> = {
|
|
2
|
+
'orders.title': 'Pedidos',
|
|
3
|
+
'orders.nav.kanban': 'Pedidos',
|
|
4
|
+
'orders.nav.list': 'Histórico',
|
|
5
|
+
'orders.nav.new': 'Novo Pedido',
|
|
6
|
+
'orders.nav.dashboard': 'Painel',
|
|
7
|
+
'orders.kanban.title': 'Pedidos',
|
|
8
|
+
'orders.kanban.subtitle': 'Pedidos ativos',
|
|
9
|
+
'orders.kanban.noOrders': 'Nenhum pedido ativo',
|
|
10
|
+
'orders.kanban.noOrdersDesc': 'Os pedidos aparecerão aqui conforme chegarem',
|
|
11
|
+
'orders.kanban.newOrder': 'Novo Pedido',
|
|
12
|
+
'orders.kanban.advance': 'Avançar',
|
|
13
|
+
'orders.kanban.cancel': 'Cancelar Pedido',
|
|
14
|
+
'orders.kanban.column.new': 'Novo',
|
|
15
|
+
'orders.kanban.column.confirmed': 'Confirmado',
|
|
16
|
+
'orders.kanban.column.preparing': 'Preparando',
|
|
17
|
+
'orders.kanban.column.ready': 'Pronto',
|
|
18
|
+
'orders.kanban.column.out': 'Saiu',
|
|
19
|
+
'orders.kanban.column.completed': 'Concluído',
|
|
20
|
+
'orders.kanban.column.cancelled': 'Cancelado',
|
|
21
|
+
'orders.detail.title': 'Detalhes do Pedido',
|
|
22
|
+
'orders.detail.items': 'Itens',
|
|
23
|
+
'orders.detail.table': 'Mesa',
|
|
24
|
+
'orders.detail.customer': 'Cliente',
|
|
25
|
+
'orders.detail.waiter': 'Garçom',
|
|
26
|
+
'orders.detail.source': 'Origem',
|
|
27
|
+
'orders.detail.subtotal': 'Subtotal',
|
|
28
|
+
'orders.detail.discount': 'Desconto',
|
|
29
|
+
'orders.detail.tax': 'Imposto',
|
|
30
|
+
'orders.detail.total': 'Total',
|
|
31
|
+
'orders.detail.notes': 'Observações',
|
|
32
|
+
'orders.newOrder.title': 'Novo Pedido',
|
|
33
|
+
'orders.newOrder.selectKind': 'Tipo de pedido',
|
|
34
|
+
'orders.newOrder.dineIn': 'Mesa',
|
|
35
|
+
'orders.newOrder.takeout': 'Para Viagem',
|
|
36
|
+
'orders.newOrder.delivery': 'Delivery',
|
|
37
|
+
'orders.newOrder.selectTable': 'Selecionar mesa',
|
|
38
|
+
'orders.newOrder.addItems': 'Adicionar itens',
|
|
39
|
+
'orders.newOrder.searchItems': 'Buscar no cardápio...',
|
|
40
|
+
'orders.newOrder.noItems': 'Nenhum item adicionado',
|
|
41
|
+
'orders.newOrder.confirm': 'Fazer Pedido',
|
|
42
|
+
'orders.newOrder.cancel': 'Cancelar',
|
|
43
|
+
'orders.list.title': 'Histórico de Pedidos',
|
|
44
|
+
'orders.list.subtitle': 'Todos os pedidos',
|
|
45
|
+
'orders.list.search': 'Buscar pedidos...',
|
|
46
|
+
'orders.list.noOrders': 'Nenhum pedido encontrado',
|
|
47
|
+
'orders.source.dine_in': 'Mesa',
|
|
48
|
+
'orders.source.takeout': 'Para Viagem',
|
|
49
|
+
'orders.source.ifood': 'iFood',
|
|
50
|
+
'orders.source.99food': '99Food',
|
|
51
|
+
'orders.source.delivery': 'Delivery',
|
|
52
|
+
'orders.settings.title': 'Configurações de Pedidos',
|
|
53
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PluginRegistryDef } from '@fayz-ai/core'
|
|
2
|
+
import type { EntityDef } from '@fayz-ai/core'
|
|
3
|
+
|
|
4
|
+
const orderSourceEntity: EntityDef = {
|
|
5
|
+
name: 'Order Source',
|
|
6
|
+
namePlural: 'Order Sources',
|
|
7
|
+
icon: 'Globe',
|
|
8
|
+
displayField: 'name',
|
|
9
|
+
defaultSort: 'name',
|
|
10
|
+
fields: [
|
|
11
|
+
{ key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
|
|
12
|
+
{ key: 'icon', label: 'Icon', type: 'text', showInTable: true },
|
|
13
|
+
{ key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
|
|
14
|
+
],
|
|
15
|
+
data: { table: 'order_sources', tenantScoped: true },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ordersRegistries: PluginRegistryDef[] = [
|
|
19
|
+
{
|
|
20
|
+
id: 'order-sources',
|
|
21
|
+
entity: orderSourceEntity,
|
|
22
|
+
icon: 'Globe',
|
|
23
|
+
description: 'Order sources and delivery platforms',
|
|
24
|
+
display: 'table',
|
|
25
|
+
seedData: [
|
|
26
|
+
{ id: 'os-dine', name: 'Dine-in', icon: '🍽️', isActive: true },
|
|
27
|
+
{ id: 'os-takeout', name: 'Takeout', icon: '🥡', isActive: true },
|
|
28
|
+
{ id: 'os-ifood', name: 'iFood', icon: '🔴', isActive: true },
|
|
29
|
+
{ id: 'os-99food', name: '99Food', icon: '🟡', isActive: true },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
]
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createStore, type StoreApi } from 'zustand/vanilla'
|
|
2
|
+
import { dedup } from '@fayz-ai/saas'
|
|
3
|
+
import { toast } from 'sonner'
|
|
4
|
+
import type { OrdersDataProvider } from './data/types'
|
|
5
|
+
import type {
|
|
6
|
+
RestaurantOrder, OrdersSummary,
|
|
7
|
+
OrderQuery, CreateOrderInput, CreateOrderItemInput,
|
|
8
|
+
UpdateOrderStatusInput,
|
|
9
|
+
} from './types'
|
|
10
|
+
import { getNextStatus } from './types'
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Store state
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface OrdersUIState {
|
|
17
|
+
// Data cache
|
|
18
|
+
orders: RestaurantOrder[]
|
|
19
|
+
ordersTotal: number
|
|
20
|
+
ordersLoading: boolean
|
|
21
|
+
orderQuery: OrderQuery
|
|
22
|
+
|
|
23
|
+
activeOrders: RestaurantOrder[]
|
|
24
|
+
activeOrdersLoading: boolean
|
|
25
|
+
|
|
26
|
+
summary: OrdersSummary | null
|
|
27
|
+
summaryLoading: boolean
|
|
28
|
+
|
|
29
|
+
selectedOrderId: string | null
|
|
30
|
+
|
|
31
|
+
// Actions
|
|
32
|
+
fetchOrders(query: OrderQuery): Promise<void>
|
|
33
|
+
fetchActiveOrders(): Promise<void>
|
|
34
|
+
fetchSummary(dateRange?: { from: string; to: string }): Promise<void>
|
|
35
|
+
selectOrder(id: string | null): void
|
|
36
|
+
createOrder(input: CreateOrderInput): Promise<RestaurantOrder>
|
|
37
|
+
advanceOrderStatus(orderId: string): Promise<void>
|
|
38
|
+
updateOrderStatus(input: UpdateOrderStatusInput): Promise<void>
|
|
39
|
+
cancelOrder(id: string, reason?: string): Promise<void>
|
|
40
|
+
addOrderItem(orderId: string, item: CreateOrderItemInput): Promise<void>
|
|
41
|
+
removeOrderItem(itemId: string): Promise<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Store factory
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export function createOrdersStore(provider: OrdersDataProvider): StoreApi<OrdersUIState> {
|
|
49
|
+
return createStore<OrdersUIState>((set, get) => ({
|
|
50
|
+
orders: [],
|
|
51
|
+
ordersTotal: 0,
|
|
52
|
+
ordersLoading: false,
|
|
53
|
+
orderQuery: {},
|
|
54
|
+
|
|
55
|
+
activeOrders: [],
|
|
56
|
+
activeOrdersLoading: false,
|
|
57
|
+
|
|
58
|
+
summary: null,
|
|
59
|
+
summaryLoading: false,
|
|
60
|
+
|
|
61
|
+
selectedOrderId: null,
|
|
62
|
+
|
|
63
|
+
async fetchOrders(query) {
|
|
64
|
+
return dedup('orders:list:' + JSON.stringify(query), async () => {
|
|
65
|
+
set({ ordersLoading: true, orderQuery: query })
|
|
66
|
+
const result = await provider.getOrders(query)
|
|
67
|
+
set({ orders: result.data, ordersTotal: result.total, ordersLoading: false })
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async fetchActiveOrders() {
|
|
72
|
+
return dedup('orders:active', async () => {
|
|
73
|
+
set({ activeOrdersLoading: true })
|
|
74
|
+
const result = await provider.getOrders({
|
|
75
|
+
status: ['new', 'confirmed', 'preparing', 'ready'],
|
|
76
|
+
})
|
|
77
|
+
set({ activeOrders: result.data, activeOrdersLoading: false })
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async fetchSummary(dateRange) {
|
|
82
|
+
return dedup('orders:summary', async () => {
|
|
83
|
+
set({ summaryLoading: true })
|
|
84
|
+
const summary = await provider.getSummary(dateRange)
|
|
85
|
+
set({ summary, summaryLoading: false })
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
selectOrder(id) {
|
|
90
|
+
set({ selectedOrderId: id })
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async createOrder(input) {
|
|
94
|
+
try {
|
|
95
|
+
const order = await provider.createOrder(input)
|
|
96
|
+
const query = get().orderQuery
|
|
97
|
+
const [result, summary] = await Promise.all([
|
|
98
|
+
provider.getOrders(query),
|
|
99
|
+
provider.getSummary(),
|
|
100
|
+
])
|
|
101
|
+
set({ orders: result.data, ordersTotal: result.total, summary })
|
|
102
|
+
toast.success('Order created')
|
|
103
|
+
return order
|
|
104
|
+
} catch (err: any) {
|
|
105
|
+
toast.error('Failed to create order', { description: err?.message })
|
|
106
|
+
throw err
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async advanceOrderStatus(orderId) {
|
|
111
|
+
try {
|
|
112
|
+
const { orders, activeOrders } = get()
|
|
113
|
+
const order = orders.find(o => o.id === orderId) ?? activeOrders.find(o => o.id === orderId)
|
|
114
|
+
if (!order) throw new Error('Order not found')
|
|
115
|
+
|
|
116
|
+
const nextStatus = getNextStatus(order.kind, order.status)
|
|
117
|
+
if (!nextStatus) throw new Error('No next status available')
|
|
118
|
+
|
|
119
|
+
await provider.updateOrderStatus({ orderId, status: nextStatus })
|
|
120
|
+
|
|
121
|
+
// Refresh active orders
|
|
122
|
+
const activeResult = await provider.getOrders({
|
|
123
|
+
status: ['new', 'confirmed', 'preparing', 'ready'],
|
|
124
|
+
})
|
|
125
|
+
set({ activeOrders: activeResult.data })
|
|
126
|
+
toast.success(`Order advanced to ${nextStatus.replace('_', ' ')}`)
|
|
127
|
+
} catch (err: any) {
|
|
128
|
+
toast.error('Failed to advance order', { description: err?.message })
|
|
129
|
+
throw err
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async updateOrderStatus(input) {
|
|
134
|
+
try {
|
|
135
|
+
await provider.updateOrderStatus(input)
|
|
136
|
+
const query = get().orderQuery
|
|
137
|
+
const [result, summary] = await Promise.all([
|
|
138
|
+
provider.getOrders(query),
|
|
139
|
+
provider.getSummary(),
|
|
140
|
+
])
|
|
141
|
+
set({ orders: result.data, ordersTotal: result.total, summary })
|
|
142
|
+
toast.success('Order status updated')
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
toast.error('Failed to update status', { description: err?.message })
|
|
145
|
+
throw err
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async cancelOrder(id, reason) {
|
|
150
|
+
try {
|
|
151
|
+
await provider.cancelOrder(id, reason)
|
|
152
|
+
const query = get().orderQuery
|
|
153
|
+
const [result, summary] = await Promise.all([
|
|
154
|
+
provider.getOrders(query),
|
|
155
|
+
provider.getSummary(),
|
|
156
|
+
])
|
|
157
|
+
set({ orders: result.data, ordersTotal: result.total, summary })
|
|
158
|
+
toast.success('Order cancelled')
|
|
159
|
+
} catch (err: any) {
|
|
160
|
+
toast.error('Failed to cancel order', { description: err?.message })
|
|
161
|
+
throw err
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
async addOrderItem(orderId, item) {
|
|
166
|
+
try {
|
|
167
|
+
await provider.addOrderItem(orderId, item)
|
|
168
|
+
const query = get().orderQuery
|
|
169
|
+
const result = await provider.getOrders(query)
|
|
170
|
+
set({ orders: result.data, ordersTotal: result.total })
|
|
171
|
+
toast.success('Item added')
|
|
172
|
+
} catch (err: any) {
|
|
173
|
+
toast.error('Failed to add item', { description: err?.message })
|
|
174
|
+
throw err
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async removeOrderItem(itemId) {
|
|
179
|
+
try {
|
|
180
|
+
await provider.removeOrderItem(itemId)
|
|
181
|
+
const query = get().orderQuery
|
|
182
|
+
const result = await provider.getOrders(query)
|
|
183
|
+
set({ orders: result.data, ordersTotal: result.total })
|
|
184
|
+
toast.success('Item removed')
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
toast.error('Failed to remove item', { description: err?.message })
|
|
187
|
+
throw err
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
}))
|
|
191
|
+
}
|