@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/OrdersContext.d.ts +40 -0
  4. package/dist/OrdersContext.d.ts.map +1 -0
  5. package/dist/OrdersPage-2IXARHDR.cjs +157 -0
  6. package/dist/OrdersPage-2IXARHDR.cjs.map +1 -0
  7. package/dist/OrdersPage-LJIEIZYE.js +155 -0
  8. package/dist/OrdersPage-LJIEIZYE.js.map +1 -0
  9. package/dist/OrdersPage.d.ts +13 -0
  10. package/dist/OrdersPage.d.ts.map +1 -0
  11. package/dist/chunk-7LFVFZKZ.js +23 -0
  12. package/dist/chunk-7LFVFZKZ.js.map +1 -0
  13. package/dist/chunk-WBGECNYU.cjs +28 -0
  14. package/dist/chunk-WBGECNYU.cjs.map +1 -0
  15. package/dist/data/fayz.d.ts +18 -0
  16. package/dist/data/fayz.d.ts.map +1 -0
  17. package/dist/data/mock.d.ts +3 -0
  18. package/dist/data/mock.d.ts.map +1 -0
  19. package/dist/data/types.d.ts +17 -0
  20. package/dist/data/types.d.ts.map +1 -0
  21. package/dist/index.cjs +1185 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.ts +42 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +1178 -0
  26. package/dist/index.js.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 +29 -0
  36. package/dist/store.d.ts.map +1 -0
  37. package/dist/types.d.ts +119 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/views/OrderKanbanView.d.ts +3 -0
  40. package/dist/views/OrderKanbanView.d.ts.map +1 -0
  41. package/package.json +54 -0
  42. package/src/OrdersContext.tsx +47 -0
  43. package/src/OrdersPage.tsx +20 -0
  44. package/src/data/fayz.ts +456 -0
  45. package/src/data/mock.ts +512 -0
  46. package/src/data/types.ts +23 -0
  47. package/src/index.ts +217 -0
  48. package/src/locales/en.ts +53 -0
  49. package/src/locales/index.ts +4 -0
  50. package/src/locales/pt-BR.ts +53 -0
  51. package/src/registries.ts +32 -0
  52. package/src/store.ts +191 -0
  53. package/src/types.ts +190 -0
  54. package/src/views/OrderKanbanView.tsx +240 -0
package/src/types.ts ADDED
@@ -0,0 +1,190 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Orders Plugin — Pure TypeScript types
3
+ // ---------------------------------------------------------------------------
4
+
5
+ // ============================================================
6
+ // ENUMS / LITERALS
7
+ // ============================================================
8
+
9
+ export type OrderKind = 'dine_in' | 'takeout' | 'delivery'
10
+
11
+ export type DineInStatus = 'new' | 'confirmed' | 'preparing' | 'ready' | 'served' | 'completed' | 'cancelled'
12
+ export type TakeoutStatus = 'new' | 'confirmed' | 'preparing' | 'ready' | 'picked_up' | 'completed' | 'cancelled'
13
+ export type DeliveryStatus = 'new' | 'confirmed' | 'preparing' | 'ready' | 'dispatched' | 'delivered' | 'completed' | 'cancelled'
14
+ export type OrderStatus = DineInStatus | TakeoutStatus | DeliveryStatus
15
+
16
+ /** Unified columns for the kanban board */
17
+ export type KanbanColumn = 'new' | 'confirmed' | 'preparing' | 'ready' | 'out' | 'completed' | 'cancelled'
18
+
19
+ // ============================================================
20
+ // STATUS FLOW
21
+ // ============================================================
22
+
23
+ export const STATUS_FLOW: Record<OrderKind, OrderStatus[]> = {
24
+ dine_in: ['new', 'confirmed', 'preparing', 'ready', 'served', 'completed'],
25
+ takeout: ['new', 'confirmed', 'preparing', 'ready', 'picked_up', 'completed'],
26
+ delivery: ['new', 'confirmed', 'preparing', 'ready', 'dispatched', 'delivered', 'completed'],
27
+ }
28
+
29
+ export function getNextStatus(kind: OrderKind, current: OrderStatus): OrderStatus | null {
30
+ const flow = STATUS_FLOW[kind]
31
+ const idx = flow.indexOf(current)
32
+ return idx >= 0 && idx < flow.length - 1 ? flow[idx + 1] : null
33
+ }
34
+
35
+ export function getPreviousStatus(kind: OrderKind, current: OrderStatus): OrderStatus | null {
36
+ const flow = STATUS_FLOW[kind]
37
+ const idx = flow.indexOf(current)
38
+ return idx > 0 ? flow[idx - 1] : null
39
+ }
40
+
41
+ export function mapToKanbanColumn(status: OrderStatus): KanbanColumn {
42
+ if (['served', 'picked_up', 'dispatched', 'delivered'].includes(status)) return 'out'
43
+ if (['new', 'confirmed', 'preparing', 'ready', 'completed', 'cancelled'].includes(status)) {
44
+ return status as KanbanColumn
45
+ }
46
+ return 'new'
47
+ }
48
+
49
+ export const KANBAN_COLUMNS: KanbanColumn[] = ['new', 'confirmed', 'preparing', 'ready', 'out', 'completed']
50
+
51
+ // ============================================================
52
+ // CORE ENTITIES
53
+ // ============================================================
54
+
55
+ /** Restaurant order — saas_core.orders + public.orders extension */
56
+ export interface RestaurantOrder {
57
+ id: string
58
+ referenceNumber: string
59
+ kind: OrderKind
60
+ status: OrderStatus
61
+ partyId?: string
62
+ partyName?: string
63
+ assigneeId?: string
64
+ assigneeName?: string
65
+ locationId?: string
66
+ subtotal: number
67
+ discount: number
68
+ tax: number
69
+ total: number
70
+ currency: string
71
+ notes?: string
72
+ tags: string[]
73
+ // Extension fields
74
+ source: string
75
+ tableId?: string
76
+ tableName?: string
77
+ estimatedMinutes?: number
78
+ items: RestaurantOrderItem[]
79
+ metadata?: Record<string, unknown>
80
+ tenantId: string
81
+ createdAt: string
82
+ updatedAt: string
83
+ completedAt?: string
84
+ }
85
+
86
+ export interface RestaurantOrderItem {
87
+ id: string
88
+ orderId: string
89
+ productId?: string
90
+ name: string
91
+ description?: string
92
+ quantity: number
93
+ unitPrice: number
94
+ discount: number
95
+ total: number
96
+ sortOrder: number
97
+ modifiers?: string
98
+ kitchenNotes?: string
99
+ metadata?: Record<string, unknown>
100
+ createdAt: string
101
+ }
102
+
103
+ // ============================================================
104
+ // INPUT TYPES
105
+ // ============================================================
106
+
107
+ export interface CreateOrderInput {
108
+ kind: OrderKind
109
+ source?: string
110
+ tableId?: string
111
+ partyId?: string
112
+ assigneeId?: string
113
+ notes?: string
114
+ estimatedMinutes?: number
115
+ items: CreateOrderItemInput[]
116
+ }
117
+
118
+ export interface CreateOrderItemInput {
119
+ productId?: string
120
+ name: string
121
+ quantity: number
122
+ unitPrice: number
123
+ discount?: number
124
+ modifiers?: string
125
+ kitchenNotes?: string
126
+ }
127
+
128
+ export interface UpdateOrderStatusInput {
129
+ orderId: string
130
+ status: OrderStatus
131
+ }
132
+
133
+ // ============================================================
134
+ // QUERY TYPES
135
+ // ============================================================
136
+
137
+ export interface OrderQuery {
138
+ kind?: OrderKind | OrderKind[]
139
+ status?: OrderStatus | OrderStatus[]
140
+ source?: string
141
+ tableId?: string
142
+ assigneeId?: string
143
+ dateRange?: { from: string; to: string }
144
+ search?: string
145
+ page?: number
146
+ pageSize?: number
147
+ }
148
+
149
+ export interface PaginatedResult<T> {
150
+ data: T[]
151
+ total: number
152
+ }
153
+
154
+ // ============================================================
155
+ // AGGREGATION
156
+ // ============================================================
157
+
158
+ export interface OrdersSummary {
159
+ todayOrderCount: number
160
+ todayRevenue: number
161
+ averageTicket: number
162
+ activeOrderCount: number
163
+ ordersByKind: Record<OrderKind, number>
164
+ ordersByStatus: Record<string, number>
165
+ topItems: Array<{ name: string; quantity: number; revenue: number }>
166
+ }
167
+
168
+ // ============================================================
169
+ // STATUS CONFIG
170
+ // ============================================================
171
+
172
+ export interface OrderStatusConfig {
173
+ value: string
174
+ label: string
175
+ color: string
176
+ icon?: string
177
+ }
178
+
179
+ export const DEFAULT_STATUS_CONFIGS: OrderStatusConfig[] = [
180
+ { value: 'new', label: 'New', color: '#6366f1', icon: 'PlusCircle' },
181
+ { value: 'confirmed', label: 'Confirmed', color: '#3b82f6', icon: 'CheckCircle' },
182
+ { value: 'preparing', label: 'Preparing', color: '#f59e0b', icon: 'ChefHat' },
183
+ { value: 'ready', label: 'Ready', color: '#10b981', icon: 'Bell' },
184
+ { value: 'served', label: 'Served', color: '#06b6d4', icon: 'Utensils' },
185
+ { value: 'picked_up', label: 'Picked Up', color: '#06b6d4', icon: 'ShoppingBag' },
186
+ { value: 'dispatched', label: 'Dispatched', color: '#8b5cf6', icon: 'Truck' },
187
+ { value: 'delivered', label: 'Delivered', color: '#06b6d4', icon: 'PackageCheck' },
188
+ { value: 'completed', label: 'Completed', color: '#22c55e', icon: 'CircleCheck' },
189
+ { value: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: 'XCircle' },
190
+ ]
@@ -0,0 +1,240 @@
1
+ import React, { useEffect, useMemo } from 'react'
2
+ import { ChevronRight, Clock, Loader2, ClipboardList } from 'lucide-react'
3
+ import { useOrdersConfig, useOrdersStore, formatCurrency } from '../OrdersContext'
4
+ import { useTranslation } from '@fayz-ai/core'
5
+ import {
6
+ KANBAN_COLUMNS,
7
+ mapToKanbanColumn,
8
+ getNextStatus,
9
+ type KanbanColumn,
10
+ type RestaurantOrder,
11
+ } from '../types'
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Column config
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const COLUMN_CONFIG: Record<KanbanColumn, { color: string }> = {
18
+ new: { color: 'bg-violet-500' },
19
+ confirmed: { color: 'bg-blue-500' },
20
+ preparing: { color: 'bg-amber-500' },
21
+ ready: { color: 'bg-emerald-500' },
22
+ out: { color: 'bg-cyan-500' },
23
+ completed: { color: 'bg-gray-400' },
24
+ cancelled: { color: 'bg-red-500' },
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function timeAgo(dateStr: string): string {
32
+ const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 60000)
33
+ if (diff < 1) return 'just now'
34
+ if (diff === 1) return '1min'
35
+ return `${diff}min`
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Order Card
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function OrderCard({
43
+ order,
44
+ onAdvance,
45
+ onSelect,
46
+ currency,
47
+ }: {
48
+ order: RestaurantOrder
49
+ onAdvance?: () => void
50
+ onSelect: () => void
51
+ currency: { code: string; locale: string; symbol: string }
52
+ }) {
53
+ const t = useTranslation()
54
+ const sourceLabel = t(`orders.source.${order.source}`) !== `orders.source.${order.source}`
55
+ ? t(`orders.source.${order.source}`)
56
+ : order.source
57
+
58
+ return (
59
+ <div
60
+ className="rounded-lg border bg-card p-3 hover:shadow-md transition-shadow cursor-pointer"
61
+ onClick={onSelect}
62
+ >
63
+ {/* Header: order number + source badge */}
64
+ <div className="flex items-center justify-between mb-2">
65
+ <span className="text-sm font-bold">{order.referenceNumber}</span>
66
+ <span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium">
67
+ {sourceLabel}
68
+ </span>
69
+ </div>
70
+
71
+ {/* Customer + table */}
72
+ {order.partyName && (
73
+ <p className="text-sm font-medium truncate">{order.partyName}</p>
74
+ )}
75
+ {order.tableName && (
76
+ <p className="text-xs text-muted-foreground">{order.tableName}</p>
77
+ )}
78
+
79
+ {/* Items */}
80
+ {order.items.length > 0 && (
81
+ <div className="mt-2 space-y-0.5">
82
+ {order.items.map((item) => (
83
+ <p key={item.id} className="text-xs text-muted-foreground truncate">
84
+ {item.quantity}x {item.name}
85
+ </p>
86
+ ))}
87
+ </div>
88
+ )}
89
+
90
+ {/* Total + time */}
91
+ <div className="flex items-center justify-between mt-3 pt-2 border-t">
92
+ <span className="text-sm font-bold">
93
+ {formatCurrency(order.total, currency)}
94
+ </span>
95
+ <span className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground">
96
+ <Clock className="h-3 w-3" />
97
+ {timeAgo(order.createdAt)}
98
+ </span>
99
+ </div>
100
+
101
+ {/* Advance button */}
102
+ {onAdvance && (
103
+ <button
104
+ type="button"
105
+ className="mt-2 flex w-full items-center justify-center gap-1 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
106
+ onClick={(e) => {
107
+ e.stopPropagation()
108
+ onAdvance()
109
+ }}
110
+ >
111
+ {t('orders.kanban.advance')}
112
+ <ChevronRight className="h-3 w-3" />
113
+ </button>
114
+ )}
115
+ </div>
116
+ )
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Skeleton column (loading state)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ function SkeletonColumn() {
124
+ return (
125
+ <div className="flex-shrink-0 w-72">
126
+ <div className="h-9 rounded-lg bg-muted animate-pulse mb-3" />
127
+ <div className="space-y-3">
128
+ {[1, 2, 3].map((i) => (
129
+ <div key={i} className="h-32 rounded-lg bg-muted/60 animate-pulse" />
130
+ ))}
131
+ </div>
132
+ </div>
133
+ )
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // OrderKanbanView
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export function OrderKanbanView() {
141
+ const t = useTranslation()
142
+ const config = useOrdersConfig()
143
+
144
+ const activeOrders = useOrdersStore((s) => s.activeOrders)
145
+ const activeOrdersLoading = useOrdersStore((s) => s.activeOrdersLoading)
146
+ const fetchActiveOrders = useOrdersStore((s) => s.fetchActiveOrders)
147
+ const advanceOrderStatus = useOrdersStore((s) => s.advanceOrderStatus)
148
+ const selectOrder = useOrdersStore((s) => s.selectOrder)
149
+
150
+ useEffect(() => {
151
+ fetchActiveOrders()
152
+ }, [fetchActiveOrders])
153
+
154
+ // Group orders by kanban column
155
+ const grouped = useMemo(() => {
156
+ const map: Record<KanbanColumn, RestaurantOrder[]> = {
157
+ new: [],
158
+ confirmed: [],
159
+ preparing: [],
160
+ ready: [],
161
+ out: [],
162
+ completed: [],
163
+ cancelled: [],
164
+ }
165
+ for (const order of activeOrders) {
166
+ const col = mapToKanbanColumn(order.status)
167
+ map[col].push(order)
168
+ }
169
+ return map
170
+ }, [activeOrders])
171
+
172
+ // Loading state
173
+ if (activeOrdersLoading && activeOrders.length === 0) {
174
+ return (
175
+ <div className="flex gap-4 overflow-x-auto pb-4">
176
+ {KANBAN_COLUMNS.map((col) => (
177
+ <SkeletonColumn key={col} />
178
+ ))}
179
+ </div>
180
+ )
181
+ }
182
+
183
+ // Empty state
184
+ if (!activeOrdersLoading && activeOrders.length === 0) {
185
+ return (
186
+ <div className="flex flex-col items-center justify-center h-64 text-center">
187
+ <ClipboardList className="h-12 w-12 text-muted-foreground/40 mb-3" />
188
+ <p className="text-sm font-medium text-muted-foreground">
189
+ {t('orders.kanban.noOrders')}
190
+ </p>
191
+ <p className="text-xs text-muted-foreground/70 mt-1">
192
+ {t('orders.kanban.noOrdersDesc')}
193
+ </p>
194
+ </div>
195
+ )
196
+ }
197
+
198
+ return (
199
+ <div className="flex gap-4 overflow-x-auto pb-4">
200
+ {KANBAN_COLUMNS.map((col) => {
201
+ const columnOrders = grouped[col]
202
+ const colConfig = COLUMN_CONFIG[col]
203
+ const label = t(`orders.kanban.column.${col}`)
204
+ const hasNext = (order: RestaurantOrder) =>
205
+ getNextStatus(order.kind, order.status) !== null
206
+
207
+ return (
208
+ <div key={col} className="flex-shrink-0 w-72">
209
+ {/* Column header */}
210
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg mb-3">
211
+ <span className={`h-2.5 w-2.5 rounded-full ${colConfig.color}`} />
212
+ <span className="text-sm font-semibold">{label}</span>
213
+ <span className="ml-auto inline-flex items-center justify-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium">
214
+ {columnOrders.length}
215
+ </span>
216
+ </div>
217
+
218
+ {/* Cards */}
219
+ <div className="space-y-3 max-h-[calc(100vh-16rem)] overflow-y-auto pr-1">
220
+ {columnOrders.map((order) => (
221
+ <OrderCard
222
+ key={order.id}
223
+ order={order}
224
+ currency={config.currency}
225
+ onSelect={() => selectOrder(order.id)}
226
+ onAdvance={hasNext(order) ? () => advanceOrderStatus(order.id) : undefined}
227
+ />
228
+ ))}
229
+ {columnOrders.length === 0 && (
230
+ <div className="text-center py-8 text-sm text-muted-foreground rounded-lg border border-dashed">
231
+ {t('orders.kanban.noOrders')}
232
+ </div>
233
+ )}
234
+ </div>
235
+ </div>
236
+ )
237
+ })}
238
+ </div>
239
+ )
240
+ }