@fayz-ai/plugin-tables 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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/TablesContext.d.ts +35 -0
  4. package/dist/TablesContext.d.ts.map +1 -0
  5. package/dist/TablesPage-5GCFGSX3.cjs +276 -0
  6. package/dist/TablesPage-5GCFGSX3.cjs.map +1 -0
  7. package/dist/TablesPage-IM732NWO.js +274 -0
  8. package/dist/TablesPage-IM732NWO.js.map +1 -0
  9. package/dist/TablesPage.d.ts +13 -0
  10. package/dist/TablesPage.d.ts.map +1 -0
  11. package/dist/data/fayz.d.ts +15 -0
  12. package/dist/data/fayz.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/types.d.ts +24 -0
  16. package/dist/data/types.d.ts.map +1 -0
  17. package/dist/index.cjs +887 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.ts +33 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +880 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/locales/en.d.ts +2 -0
  24. package/dist/locales/en.d.ts.map +1 -0
  25. package/dist/locales/index.d.ts +2 -0
  26. package/dist/locales/index.d.ts.map +1 -0
  27. package/dist/locales/pt-BR.d.ts +2 -0
  28. package/dist/locales/pt-BR.d.ts.map +1 -0
  29. package/dist/registries.d.ts +3 -0
  30. package/dist/registries.d.ts.map +1 -0
  31. package/dist/store.d.ts +28 -0
  32. package/dist/store.d.ts.map +1 -0
  33. package/dist/types.d.ts +84 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/views/FloorPlanView.d.ts +3 -0
  36. package/dist/views/FloorPlanView.d.ts.map +1 -0
  37. package/package.json +55 -0
  38. package/src/TablesContext.tsx +41 -0
  39. package/src/TablesPage.tsx +20 -0
  40. package/src/data/fayz.ts +348 -0
  41. package/src/data/mock.ts +322 -0
  42. package/src/data/types.ts +31 -0
  43. package/src/index.ts +179 -0
  44. package/src/locales/en.ts +45 -0
  45. package/src/locales/index.ts +4 -0
  46. package/src/locales/pt-BR.ts +45 -0
  47. package/src/registries.ts +32 -0
  48. package/src/store.ts +178 -0
  49. package/src/types.ts +116 -0
  50. package/src/views/FloorPlanView.tsx +388 -0
@@ -0,0 +1,388 @@
1
+ import React, { useEffect, useMemo } from 'react'
2
+ import { MapPin, Users, Clock, UtensilsCrossed, Loader2 } from 'lucide-react'
3
+ import { useTablesConfig, useTablesStore } from '../TablesContext'
4
+ import { useTranslation } from '@fayz-ai/core'
5
+ import type { RestaurantTable, TableStatus } from '../types'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Status config
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const statusConfig: Record<TableStatus, { color: string; bg: string }> = {
12
+ available: { color: 'text-success', bg: 'bg-success/10 border-success/30 hover:bg-success/20' },
13
+ occupied: { color: 'text-primary', bg: 'bg-primary/10 border-primary/30 hover:bg-primary/20' },
14
+ reserved: { color: 'text-accent', bg: 'bg-accent/10 border-accent/30 hover:bg-accent/20' },
15
+ cleaning: { color: 'text-muted-foreground', bg: 'bg-muted border-border hover:bg-muted/80' },
16
+ }
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function formatElapsed(minutes: number | undefined, minLabel: string): string {
23
+ if (minutes == null) return ''
24
+ if (minutes < 60) return `${minutes}${minLabel}`
25
+ const h = Math.floor(minutes / 60)
26
+ const m = minutes % 60
27
+ return m > 0 ? `${h}h ${m}${minLabel}` : `${h}h`
28
+ }
29
+
30
+ function formatCurrency(value: number | undefined): string {
31
+ if (value == null) return ''
32
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency: 'BRL' }).format(value)
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Table card
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function TableCard({ table, isSelected, onClick, t }: {
40
+ table: RestaurantTable
41
+ isSelected: boolean
42
+ onClick: () => void
43
+ t: (key: string) => string
44
+ }) {
45
+ const config = statusConfig[table.status]
46
+ const isLarge = table.seats >= 6
47
+ const minLabel = t('tables.floorPlan.min')
48
+
49
+ return (
50
+ <button
51
+ onClick={onClick}
52
+ className={`
53
+ relative flex flex-col items-center justify-center rounded-lg border-2 p-3 transition-all cursor-pointer
54
+ ${config.bg}
55
+ ${isLarge ? 'col-span-2' : ''}
56
+ ${isSelected ? 'ring-2 ring-primary ring-offset-2' : ''}
57
+ `}
58
+ style={{ minHeight: '120px' }}
59
+ >
60
+ <span className={`text-2xl font-bold ${config.color}`}>{table.number}</span>
61
+ <span className="text-xs text-muted-foreground mt-1">
62
+ {table.seats} {t('tables.floorPlan.seats')}
63
+ </span>
64
+
65
+ {table.status === 'occupied' && (
66
+ <>
67
+ <span className="text-xs font-medium mt-1">
68
+ {table.currentGuests} {t('tables.detail.guests').toLowerCase()}
69
+ </span>
70
+ <span className="text-[10px] text-muted-foreground">
71
+ {formatElapsed(table.currentElapsedMinutes, minLabel)}
72
+ </span>
73
+ </>
74
+ )}
75
+
76
+ <span
77
+ className={`
78
+ absolute -top-2 -right-2 text-[10px] px-1.5 py-0.5 rounded-full font-medium border
79
+ ${table.status === 'occupied'
80
+ ? 'bg-primary text-primary-foreground border-primary'
81
+ : 'bg-secondary text-secondary-foreground border-border'}
82
+ `}
83
+ >
84
+ {t(`tables.floorPlan.${table.status}`)}
85
+ </span>
86
+ </button>
87
+ )
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Detail sidebar
92
+ // ---------------------------------------------------------------------------
93
+
94
+ function DetailPanel({ table, t }: { table: RestaurantTable; t: (key: string) => string }) {
95
+ const updateTableStatus = useTablesStore(s => s.updateTableStatus)
96
+ const seatGuests = useTablesStore(s => s.seatGuests)
97
+ const closeSession = useTablesStore(s => s.closeSession)
98
+ const minLabel = t('tables.floorPlan.min')
99
+
100
+ return (
101
+ <div className="rounded-lg border bg-card p-5 sticky top-6">
102
+ <div className="flex items-center justify-between mb-4">
103
+ <h3 className="text-lg font-bold">
104
+ {table.name || `#${table.number}`}
105
+ </h3>
106
+ <span
107
+ className={`
108
+ text-xs px-2 py-1 rounded-full font-medium border
109
+ ${table.status === 'occupied'
110
+ ? 'bg-primary text-primary-foreground border-primary'
111
+ : 'bg-secondary text-secondary-foreground border-border'}
112
+ `}
113
+ >
114
+ {t(`tables.floorPlan.${table.status}`)}
115
+ </span>
116
+ </div>
117
+
118
+ <dl className="space-y-3 text-sm">
119
+ <div className="flex justify-between">
120
+ <dt className="text-muted-foreground">{t('tables.floorPlan.seats')}</dt>
121
+ <dd className="font-medium">{table.seats}</dd>
122
+ </div>
123
+ {table.zoneName && (
124
+ <div className="flex justify-between">
125
+ <dt className="text-muted-foreground">{t('tables.floorPlan.zone')}</dt>
126
+ <dd className="font-medium">{table.zoneName}</dd>
127
+ </div>
128
+ )}
129
+ {table.status === 'occupied' && (
130
+ <>
131
+ {table.currentGuests != null && (
132
+ <div className="flex justify-between">
133
+ <dt className="text-muted-foreground">{t('tables.detail.guests')}</dt>
134
+ <dd className="font-medium">{table.currentGuests}</dd>
135
+ </div>
136
+ )}
137
+ {table.currentWaiterName && (
138
+ <div className="flex justify-between">
139
+ <dt className="text-muted-foreground">{t('tables.detail.waiter')}</dt>
140
+ <dd className="font-medium">{table.currentWaiterName}</dd>
141
+ </div>
142
+ )}
143
+ {table.currentOrderId && (
144
+ <div className="flex justify-between">
145
+ <dt className="text-muted-foreground">{t('tables.floorPlan.order')}</dt>
146
+ <dd className="font-medium">{table.currentOrderId}</dd>
147
+ </div>
148
+ )}
149
+ {table.currentTotal != null && (
150
+ <div className="flex justify-between">
151
+ <dt className="text-muted-foreground">{t('tables.detail.total')}</dt>
152
+ <dd className="font-bold text-base">{formatCurrency(table.currentTotal)}</dd>
153
+ </div>
154
+ )}
155
+ {table.currentElapsedMinutes != null && (
156
+ <div className="flex justify-between">
157
+ <dt className="text-muted-foreground">{t('tables.detail.elapsed')}</dt>
158
+ <dd className="font-medium">{formatElapsed(table.currentElapsedMinutes, minLabel)}</dd>
159
+ </div>
160
+ )}
161
+ </>
162
+ )}
163
+ </dl>
164
+
165
+ <div className="mt-5 grid grid-cols-2 gap-2">
166
+ {table.status === 'available' && (
167
+ <button
168
+ className="col-span-2 inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
169
+ onClick={() => seatGuests({ tableId: table.id, guests: 2 })}
170
+ >
171
+ {t('tables.detail.seatGuests')}
172
+ </button>
173
+ )}
174
+ {table.status === 'occupied' && (
175
+ <>
176
+ <button className="inline-flex items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors">
177
+ {t('tables.detail.viewOrder')}
178
+ </button>
179
+ <button
180
+ className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
181
+ onClick={() => {
182
+ if (table.currentSessionId) closeSession(table.currentSessionId)
183
+ }}
184
+ >
185
+ {t('tables.detail.closeTable')}
186
+ </button>
187
+ </>
188
+ )}
189
+ {table.status === 'cleaning' && (
190
+ <button
191
+ className="col-span-2 inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
192
+ onClick={() => updateTableStatus({ tableId: table.id, status: 'available' })}
193
+ >
194
+ {t('tables.detail.markClean')}
195
+ </button>
196
+ )}
197
+ {table.status === 'reserved' && (
198
+ <>
199
+ <button
200
+ className="inline-flex items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors"
201
+ onClick={() => updateTableStatus({ tableId: table.id, status: 'available' })}
202
+ >
203
+ {t('tables.detail.cancel')}
204
+ </button>
205
+ <button
206
+ className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
207
+ onClick={() => seatGuests({ tableId: table.id, guests: 2 })}
208
+ >
209
+ {t('tables.floorPlan.checkIn')}
210
+ </button>
211
+ </>
212
+ )}
213
+ </div>
214
+ </div>
215
+ )
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Loading skeleton
220
+ // ---------------------------------------------------------------------------
221
+
222
+ function FloorPlanSkeleton() {
223
+ return (
224
+ <div className="space-y-6">
225
+ <div className="flex items-center gap-3">
226
+ {Array.from({ length: 4 }).map((_, i) => (
227
+ <div key={i} className="h-4 w-20 rounded bg-muted animate-pulse" />
228
+ ))}
229
+ </div>
230
+ <div className="grid lg:grid-cols-[1fr_320px] gap-6">
231
+ <div className="space-y-6">
232
+ {Array.from({ length: 2 }).map((_, z) => (
233
+ <div key={z} className="rounded-lg border bg-card p-5">
234
+ <div className="h-4 w-24 rounded bg-muted animate-pulse mb-4" />
235
+ <div className="grid grid-cols-4 gap-3">
236
+ {Array.from({ length: 4 }).map((_, i) => (
237
+ <div key={i} className="h-[120px] rounded-lg bg-muted animate-pulse" />
238
+ ))}
239
+ </div>
240
+ </div>
241
+ ))}
242
+ </div>
243
+ <div className="h-[300px] rounded-lg bg-muted animate-pulse" />
244
+ </div>
245
+ </div>
246
+ )
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Main view
251
+ // ---------------------------------------------------------------------------
252
+
253
+ export function FloorPlanView() {
254
+ const t = useTranslation()
255
+ const config = useTablesConfig()
256
+
257
+ const tables = useTablesStore(s => s.tables)
258
+ const zones = useTablesStore(s => s.zones)
259
+ const tablesLoading = useTablesStore(s => s.tablesLoading)
260
+ const fetchTables = useTablesStore(s => s.fetchTables)
261
+ const fetchZones = useTablesStore(s => s.fetchZones)
262
+ const selectedTableId = useTablesStore(s => s.selectedTableId)
263
+ const selectTable = useTablesStore(s => s.selectTable)
264
+
265
+ // Fetch on mount
266
+ useEffect(() => {
267
+ fetchTables()
268
+ fetchZones()
269
+ }, [fetchTables, fetchZones])
270
+
271
+ // Group tables by zone
272
+ const tablesByZone = useMemo(() => {
273
+ const map = new Map<string, { zoneName: string; color?: string; tables: RestaurantTable[] }>()
274
+
275
+ // Seed zone order from store zones
276
+ for (const zone of zones) {
277
+ map.set(zone.id, { zoneName: zone.name, color: zone.color, tables: [] })
278
+ }
279
+
280
+ for (const table of tables) {
281
+ const zoneId = table.zone
282
+ if (!map.has(zoneId)) {
283
+ map.set(zoneId, { zoneName: table.zoneName || zoneId, tables: [] })
284
+ }
285
+ map.get(zoneId)!.tables.push(table)
286
+ }
287
+
288
+ // Filter out empty zones
289
+ return Array.from(map.entries())
290
+ .filter(([, v]) => v.tables.length > 0)
291
+ .map(([id, v]) => ({ id, ...v }))
292
+ }, [tables, zones])
293
+
294
+ // Summary counts
295
+ const occupiedCount = tables.filter(t => t.status === 'occupied').length
296
+ const availableCount = tables.filter(t => t.status === 'available').length
297
+
298
+ // Selected table
299
+ const selectedTable = selectedTableId
300
+ ? tables.find(t => t.id === selectedTableId) ?? null
301
+ : null
302
+
303
+ // Loading state
304
+ if (tablesLoading && tables.length === 0) {
305
+ return <FloorPlanSkeleton />
306
+ }
307
+
308
+ // Empty state
309
+ if (!tablesLoading && tables.length === 0) {
310
+ return (
311
+ <div className="flex flex-col items-center justify-center py-20 text-center">
312
+ <UtensilsCrossed className="h-12 w-12 text-muted-foreground/40 mb-4" />
313
+ <h3 className="text-lg font-semibold">{t('tables.floorPlan.noTables')}</h3>
314
+ <p className="text-sm text-muted-foreground mt-1">{t('tables.floorPlan.noTablesDesc')}</p>
315
+ </div>
316
+ )
317
+ }
318
+
319
+ return (
320
+ <div className="space-y-6">
321
+ {/* Header: summary + status legend */}
322
+ <div className="flex items-center justify-between flex-wrap gap-4">
323
+ <div>
324
+ <p className="text-muted-foreground text-sm">
325
+ {t('tables.floorPlan.occupiedOf')
326
+ .replace('{occupied}', String(occupiedCount))
327
+ .replace('{available}', String(availableCount))
328
+ .replace('{total}', String(tables.length))}
329
+ </p>
330
+ </div>
331
+ <div className="flex gap-3 flex-wrap">
332
+ {(Object.keys(statusConfig) as TableStatus[]).map(status => (
333
+ <div key={status} className="flex items-center gap-1.5 text-xs">
334
+ <div className={`h-3 w-3 rounded-full ${statusConfig[status].bg} border`} />
335
+ <span className="text-muted-foreground">{t(`tables.floorPlan.${status}`)}</span>
336
+ </div>
337
+ ))}
338
+ </div>
339
+ </div>
340
+
341
+ {/* Floor plan grid + detail panel */}
342
+ <div className="grid lg:grid-cols-[1fr_320px] gap-6">
343
+ {/* Zone sections */}
344
+ <div className="space-y-6">
345
+ {tablesByZone.map(zone => (
346
+ <div key={zone.id} className="rounded-lg border bg-card p-5">
347
+ <div className="flex items-center gap-2 mb-4">
348
+ {zone.color && (
349
+ <div
350
+ className="h-3 w-3 rounded-full border"
351
+ style={{ backgroundColor: zone.color }}
352
+ />
353
+ )}
354
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
355
+ {zone.zoneName}
356
+ </h3>
357
+ </div>
358
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
359
+ {zone.tables.map(table => (
360
+ <TableCard
361
+ key={table.id}
362
+ table={table}
363
+ isSelected={table.id === selectedTableId}
364
+ onClick={() => selectTable(table.id === selectedTableId ? null : table.id)}
365
+ t={t}
366
+ />
367
+ ))}
368
+ </div>
369
+ </div>
370
+ ))}
371
+ </div>
372
+
373
+ {/* Detail panel / sidebar */}
374
+ <div>
375
+ {selectedTable ? (
376
+ <DetailPanel table={selectedTable} t={t} />
377
+ ) : (
378
+ <div className="rounded-lg border bg-card p-8 text-center text-muted-foreground">
379
+ <MapPin className="h-10 w-10 mx-auto mb-3 text-muted-foreground/40" />
380
+ <p className="font-medium">{t('tables.floorPlan.selectTable')}</p>
381
+ <p className="text-sm mt-1">{t('tables.floorPlan.selectTableDesc')}</p>
382
+ </div>
383
+ )}
384
+ </div>
385
+ </div>
386
+ </div>
387
+ )
388
+ }