@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,348 @@
1
+ import { createFayzClient, type FayzClientOptions, type FayzTableFilter } from '@fayz-ai/sdk'
2
+ import type {
3
+ CreateTableInput,
4
+ RestaurantTable,
5
+ SeatGuestsInput,
6
+ TableQuery,
7
+ TableSession,
8
+ TablesSummary,
9
+ TableStatus,
10
+ UpdateTableStatusInput,
11
+ Zone,
12
+ } from '../types'
13
+ import type { TablesDataProvider } from './types'
14
+
15
+ export interface FayzTablesProviderOptions extends FayzClientOptions {
16
+ projectId?: string
17
+ tableName?: string
18
+ schema?: string
19
+ runtime?: boolean
20
+ /**
21
+ * Active tenant id (or a getter) used to scope reads and stamp writes on the
22
+ * restaurant_tables table. When omitted, scoping is left to runtime/RLS.
23
+ */
24
+ tenantId?: string | (() => string | undefined | null)
25
+ }
26
+
27
+ interface RestaurantTableRow {
28
+ id: string
29
+ tenant_id?: string | null
30
+ name?: string | null
31
+ number?: number | null
32
+ seats?: number | null
33
+ status?: TableStatus | null
34
+ zone?: string | null
35
+ zone_name?: string | null
36
+ shape?: RestaurantTable['shape'] | null
37
+ grid_col?: number | null
38
+ grid_row?: number | null
39
+ is_active?: boolean | null
40
+ current_session_id?: string | null
41
+ current_order_id?: string | null
42
+ current_guests?: number | null
43
+ current_waiter_name?: string | null
44
+ current_elapsed_minutes?: number | null
45
+ current_total?: number | null
46
+ metadata?: Record<string, unknown> | null
47
+ created_at?: string | null
48
+ updated_at?: string | null
49
+ }
50
+
51
+ const DEFAULT_TABLE_NAME = 'restaurant_tables'
52
+
53
+ function nowIso(): string {
54
+ return new Date().toISOString()
55
+ }
56
+
57
+ function rowToTable(row: RestaurantTableRow): RestaurantTable {
58
+ const number = row.number ?? 0
59
+ const zone = row.zone ?? 'indoor'
60
+ return {
61
+ id: row.id,
62
+ name: row.name ?? `Table ${number}`,
63
+ number,
64
+ seats: row.seats ?? 4,
65
+ status: row.status ?? 'available',
66
+ zone,
67
+ zoneName: row.zone_name ?? zone,
68
+ shape: row.shape ?? 'square',
69
+ gridCol: row.grid_col ?? 0,
70
+ gridRow: row.grid_row ?? 0,
71
+ isActive: row.is_active ?? true,
72
+ currentSessionId: row.current_session_id ?? undefined,
73
+ currentOrderId: row.current_order_id ?? undefined,
74
+ currentGuests: row.current_guests ?? undefined,
75
+ currentWaiterName: row.current_waiter_name ?? undefined,
76
+ currentElapsedMinutes: row.current_elapsed_minutes ?? undefined,
77
+ currentTotal: row.current_total ?? undefined,
78
+ metadata: row.metadata ?? {},
79
+ tenantId: row.tenant_id ?? 'runtime-tenant',
80
+ createdAt: row.created_at ?? nowIso(),
81
+ updatedAt: row.updated_at ?? row.created_at ?? nowIso(),
82
+ }
83
+ }
84
+
85
+ function tableCreatePayload(input: CreateTableInput): Record<string, unknown> {
86
+ return {
87
+ name: input.name,
88
+ number: input.number,
89
+ seats: input.seats,
90
+ zone: input.zone,
91
+ shape: input.shape ?? 'square',
92
+ grid_col: input.gridCol ?? 0,
93
+ grid_row: input.gridRow ?? 0,
94
+ status: 'available',
95
+ is_active: true,
96
+ }
97
+ }
98
+
99
+ function tableUpdatePayload(data: Partial<RestaurantTable>): Record<string, unknown> {
100
+ const payload: Record<string, unknown> = {}
101
+ if (data.name !== undefined) payload.name = data.name
102
+ if (data.number !== undefined) payload.number = data.number
103
+ if (data.seats !== undefined) payload.seats = data.seats
104
+ if (data.status !== undefined) payload.status = data.status
105
+ if (data.zone !== undefined) payload.zone = data.zone
106
+ if (data.zoneName !== undefined) payload.zone_name = data.zoneName
107
+ if (data.shape !== undefined) payload.shape = data.shape
108
+ if (data.gridCol !== undefined) payload.grid_col = data.gridCol
109
+ if (data.gridRow !== undefined) payload.grid_row = data.gridRow
110
+ if (data.isActive !== undefined) payload.is_active = data.isActive
111
+ if (data.currentSessionId !== undefined) payload.current_session_id = data.currentSessionId
112
+ if (data.currentOrderId !== undefined) payload.current_order_id = data.currentOrderId
113
+ if (data.currentGuests !== undefined) payload.current_guests = data.currentGuests
114
+ if (data.currentWaiterName !== undefined) payload.current_waiter_name = data.currentWaiterName
115
+ if (data.currentElapsedMinutes !== undefined) payload.current_elapsed_minutes = data.currentElapsedMinutes
116
+ if (data.currentTotal !== undefined) payload.current_total = data.currentTotal
117
+ if (data.metadata !== undefined) payload.metadata = data.metadata
118
+ return payload
119
+ }
120
+
121
+ function filtersFromQuery(query?: TableQuery): FayzTableFilter[] {
122
+ const filters: FayzTableFilter[] = [{ column: 'is_active', operator: 'eq', value: true }]
123
+ if (!query) return filters
124
+ if (query.zone) filters.push({ column: 'zone', operator: 'eq', value: query.zone })
125
+ if (query.status && !Array.isArray(query.status)) {
126
+ filters.push({ column: 'status', operator: 'eq', value: query.status })
127
+ }
128
+ return filters
129
+ }
130
+
131
+ function matchesClientQuery(table: RestaurantTable, query?: TableQuery): boolean {
132
+ if (!query) return true
133
+ if (Array.isArray(query.status) && !query.status.includes(table.status)) return false
134
+ if (query.search) {
135
+ const search = query.search.toLowerCase()
136
+ return table.name.toLowerCase().includes(search)
137
+ || String(table.number).includes(search)
138
+ || (table.zoneName?.toLowerCase().includes(search) ?? false)
139
+ }
140
+ return true
141
+ }
142
+
143
+ function summarize(tables: RestaurantTable[]): TablesSummary {
144
+ const availableCount = tables.filter((table) => table.status === 'available').length
145
+ const occupiedCount = tables.filter((table) => table.status === 'occupied').length
146
+ const reservedCount = tables.filter((table) => table.status === 'reserved').length
147
+ const cleaningCount = tables.filter((table) => table.status === 'cleaning').length
148
+ const totalSeats = tables.reduce((sum, table) => sum + table.seats, 0)
149
+ const occupiedSeats = tables
150
+ .filter((table) => table.status === 'occupied')
151
+ .reduce((sum, table) => sum + (table.currentGuests ?? table.seats), 0)
152
+
153
+ return {
154
+ totalTables: tables.length,
155
+ availableCount,
156
+ occupiedCount,
157
+ reservedCount,
158
+ cleaningCount,
159
+ totalSeats,
160
+ occupiedSeats,
161
+ averageSessionMinutes: 0,
162
+ }
163
+ }
164
+
165
+ export function createFayzTablesProvider(options: FayzTablesProviderOptions = {}): TablesDataProvider {
166
+ const client = createFayzClient(options)
167
+ const table = options.tableName ?? DEFAULT_TABLE_NAME
168
+ const baseOptions = {
169
+ projectId: options.projectId,
170
+ table,
171
+ schema: options.schema,
172
+ runtime: options.runtime,
173
+ }
174
+
175
+ function resolveTenant(): string | undefined {
176
+ const value = typeof options.tenantId === 'function' ? options.tenantId() : options.tenantId
177
+ return value ?? undefined
178
+ }
179
+
180
+ function withTenant(filters: FayzTableFilter[]): FayzTableFilter[] {
181
+ const tenantId = resolveTenant()
182
+ return tenantId ? [{ column: 'tenant_id', operator: 'eq', value: tenantId }, ...filters] : filters
183
+ }
184
+
185
+ async function listTables(query?: TableQuery): Promise<RestaurantTable[]> {
186
+ const response = await client.data.listRows<RestaurantTableRow>({
187
+ ...baseOptions,
188
+ filters: withTenant(filtersFromQuery(query)),
189
+ sortColumn: 'number',
190
+ sortDirection: 'asc',
191
+ limit: 500,
192
+ })
193
+ return response.rows.map(rowToTable).filter((item) => matchesClientQuery(item, query))
194
+ }
195
+
196
+ async function getById(id: string): Promise<RestaurantTable | null> {
197
+ const response = await client.data.listRows<RestaurantTableRow>({
198
+ ...baseOptions,
199
+ filters: withTenant([{ column: 'id', operator: 'eq', value: id }]),
200
+ limit: 1,
201
+ })
202
+ return response.rows[0] ? rowToTable(response.rows[0]) : null
203
+ }
204
+
205
+ return {
206
+ async getTables(query?: TableQuery) {
207
+ return listTables(query)
208
+ },
209
+
210
+ async getTableById(id: string) {
211
+ return getById(id)
212
+ },
213
+
214
+ async createTable(input: CreateTableInput) {
215
+ const tenantId = resolveTenant()
216
+ const row = await client.data.createRow<RestaurantTableRow>({
217
+ ...baseOptions,
218
+ row: tenantId ? { ...tableCreatePayload(input), tenant_id: tenantId } : tableCreatePayload(input),
219
+ })
220
+ return rowToTable(row)
221
+ },
222
+
223
+ async updateTable(id: string, data: Partial<RestaurantTable>) {
224
+ const row = await client.data.updateRow<RestaurantTableRow>({
225
+ ...baseOptions,
226
+ primaryKeys: { id },
227
+ row: tableUpdatePayload(data),
228
+ })
229
+ return rowToTable(row)
230
+ },
231
+
232
+ async deleteTable(id: string) {
233
+ await client.data.deleteRows({ ...baseOptions, rows: [{ id }] })
234
+ },
235
+
236
+ async updateTableStatus(input: UpdateTableStatusInput) {
237
+ return this.updateTable(input.tableId, { status: input.status })
238
+ },
239
+
240
+ async updateTablePositions(positions: Array<{ id: string; gridCol: number; gridRow: number }>) {
241
+ await Promise.all(positions.map((position) => this.updateTable(position.id, {
242
+ gridCol: position.gridCol,
243
+ gridRow: position.gridRow,
244
+ })))
245
+ },
246
+
247
+ async seatGuests(input: SeatGuestsInput) {
248
+ const table = await this.updateTable(input.tableId, {
249
+ status: 'occupied',
250
+ currentGuests: input.guests,
251
+ currentWaiterName: input.waiterId ? `Waiter ${input.waiterId}` : undefined,
252
+ currentElapsedMinutes: 0,
253
+ currentTotal: 0,
254
+ })
255
+ const session: TableSession = {
256
+ id: table.currentSessionId ?? `runtime-session-${table.id}`,
257
+ tableId: table.id,
258
+ tableName: table.name,
259
+ guests: input.guests,
260
+ waiterId: input.waiterId,
261
+ waiterName: table.currentWaiterName,
262
+ seatedAt: nowIso(),
263
+ status: 'active',
264
+ notes: input.notes,
265
+ tenantId: table.tenantId,
266
+ createdAt: nowIso(),
267
+ }
268
+ return session
269
+ },
270
+
271
+ async closeSession(sessionId: string) {
272
+ const tables = await listTables()
273
+ const table = tables.find((item) => item.currentSessionId === sessionId)
274
+ if (table) await this.updateTable(table.id, {
275
+ status: 'cleaning',
276
+ currentSessionId: undefined,
277
+ currentOrderId: undefined,
278
+ currentGuests: undefined,
279
+ currentWaiterName: undefined,
280
+ currentElapsedMinutes: undefined,
281
+ currentTotal: undefined,
282
+ })
283
+ return {
284
+ id: sessionId,
285
+ tableId: table?.id ?? '',
286
+ tableName: table?.name,
287
+ guests: table?.currentGuests ?? 0,
288
+ seatedAt: nowIso(),
289
+ closedAt: nowIso(),
290
+ status: 'closed',
291
+ tenantId: table?.tenantId ?? 'runtime-tenant',
292
+ createdAt: nowIso(),
293
+ }
294
+ },
295
+
296
+ async getActiveSessions() {
297
+ const tables = await listTables({ status: 'occupied' })
298
+ return tables.map((table) => ({
299
+ id: table.currentSessionId ?? `runtime-session-${table.id}`,
300
+ tableId: table.id,
301
+ tableName: table.name,
302
+ guests: table.currentGuests ?? table.seats,
303
+ waiterName: table.currentWaiterName,
304
+ seatedAt: nowIso(),
305
+ status: 'active' as const,
306
+ tenantId: table.tenantId,
307
+ createdAt: nowIso(),
308
+ }))
309
+ },
310
+
311
+ async getSessionHistory() {
312
+ return []
313
+ },
314
+
315
+ async getZones() {
316
+ const tables = await listTables()
317
+ const zones = new Map<string, Zone>()
318
+ for (const table of tables) {
319
+ if (zones.has(table.zone)) continue
320
+ zones.set(table.zone, {
321
+ id: table.zone,
322
+ name: table.zoneName ?? table.zone,
323
+ sortOrder: zones.size,
324
+ isActive: true,
325
+ tenantId: table.tenantId,
326
+ createdAt: table.createdAt,
327
+ })
328
+ }
329
+ return [...zones.values()]
330
+ },
331
+
332
+ async createZone() {
333
+ throw new Error('Restaurant zone writes require a dedicated Fayz table/broker contract.')
334
+ },
335
+
336
+ async updateZone() {
337
+ throw new Error('Restaurant zone writes require a dedicated Fayz table/broker contract.')
338
+ },
339
+
340
+ async deleteZone() {
341
+ throw new Error('Restaurant zone writes require a dedicated Fayz table/broker contract.')
342
+ },
343
+
344
+ async getSummary() {
345
+ return summarize(await listTables())
346
+ },
347
+ }
348
+ }
@@ -0,0 +1,322 @@
1
+ import type {
2
+ RestaurantTable, TableSession, Zone,
3
+ CreateTableInput, SeatGuestsInput, UpdateTableStatusInput,
4
+ TableQuery, TablesSummary, TableShape,
5
+ } from '../types'
6
+ import type { TablesDataProvider } from './types'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // ID generator
10
+ // ---------------------------------------------------------------------------
11
+
12
+ let uid = 0
13
+ function nextId(prefix: string) { return prefix + '-' + (++uid) }
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Seed helpers
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const TENANT = 'mock-tenant'
20
+ const NOW = new Date().toISOString()
21
+
22
+ function makeTable(
23
+ id: string,
24
+ number: number,
25
+ seats: number,
26
+ zone: string,
27
+ zoneName: string,
28
+ shape: TableShape,
29
+ gridCol: number,
30
+ gridRow: number,
31
+ ): RestaurantTable {
32
+ return {
33
+ id,
34
+ name: `Table ${number}`,
35
+ number,
36
+ seats,
37
+ status: 'available',
38
+ zone,
39
+ zoneName,
40
+ shape,
41
+ gridCol,
42
+ gridRow,
43
+ isActive: true,
44
+ tenantId: TENANT,
45
+ createdAt: NOW,
46
+ updatedAt: NOW,
47
+ }
48
+ }
49
+
50
+ function seedZones(): Zone[] {
51
+ return [
52
+ { id: nextId('zone'), name: 'Indoor', color: '#3b82f6', sortOrder: 0, isActive: true, tenantId: TENANT, createdAt: NOW },
53
+ { id: nextId('zone'), name: 'Outdoor', color: '#22c55e', sortOrder: 1, isActive: true, tenantId: TENANT, createdAt: NOW },
54
+ { id: nextId('zone'), name: 'Bar', color: '#f59e0b', sortOrder: 2, isActive: true, tenantId: TENANT, createdAt: NOW },
55
+ ]
56
+ }
57
+
58
+ function seedTables(zones: Zone[]): RestaurantTable[] {
59
+ const [indoor, outdoor, bar] = zones
60
+
61
+ const tables: RestaurantTable[] = []
62
+
63
+ // Indoor zone — tables 1-8, 4x2 grid, alternating 4/6 seats, square/rectangle
64
+ for (let i = 0; i < 8; i++) {
65
+ const num = i + 1
66
+ tables.push(makeTable(
67
+ nextId('table'),
68
+ num,
69
+ i % 2 === 0 ? 4 : 6,
70
+ indoor.id,
71
+ indoor.name,
72
+ i % 2 === 0 ? 'square' : 'rectangle',
73
+ (i % 4),
74
+ Math.floor(i / 4),
75
+ ))
76
+ }
77
+
78
+ // Outdoor zone — tables 9-10, round, 4 seats
79
+ tables.push(makeTable(nextId('table'), 9, 4, outdoor.id, outdoor.name, 'round', 0, 0))
80
+ tables.push(makeTable(nextId('table'), 10, 4, outdoor.id, outdoor.name, 'round', 1, 0))
81
+
82
+ // Bar zone — tables 11-12, bar shape, 2 seats
83
+ tables.push(makeTable(nextId('table'), 11, 2, bar.id, bar.name, 'bar', 0, 0))
84
+ tables.push(makeTable(nextId('table'), 12, 2, bar.id, bar.name, 'bar', 1, 0))
85
+
86
+ return tables
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Provider factory
91
+ // ---------------------------------------------------------------------------
92
+
93
+ export function createMockTablesProvider(): TablesDataProvider {
94
+ const zones = seedZones()
95
+ const tables = seedTables(zones)
96
+ const sessions: TableSession[] = []
97
+
98
+ // ------- helpers -------
99
+
100
+ function findTable(id: string) {
101
+ return tables.find(t => t.id === id) ?? null
102
+ }
103
+
104
+ function matchesQuery(table: RestaurantTable, query?: TableQuery): boolean {
105
+ if (!query) return true
106
+ if (query.zone && table.zone !== query.zone) return false
107
+ if (query.status) {
108
+ const statuses = Array.isArray(query.status) ? query.status : [query.status]
109
+ if (!statuses.includes(table.status)) return false
110
+ }
111
+ if (query.search) {
112
+ const s = query.search.toLowerCase()
113
+ if (
114
+ !table.name.toLowerCase().includes(s) &&
115
+ !String(table.number).includes(s) &&
116
+ !(table.zoneName ?? '').toLowerCase().includes(s)
117
+ ) return false
118
+ }
119
+ return true
120
+ }
121
+
122
+ // ------- provider -------
123
+
124
+ return {
125
+ // ---- Tables ----
126
+
127
+ async getTables(query?: TableQuery) {
128
+ return tables.filter(t => matchesQuery(t, query))
129
+ },
130
+
131
+ async getTableById(id: string) {
132
+ return findTable(id)
133
+ },
134
+
135
+ async createTable(input: CreateTableInput) {
136
+ const zone = zones.find(z => z.id === input.zone)
137
+ const table: RestaurantTable = {
138
+ id: nextId('table'),
139
+ name: input.name,
140
+ number: input.number,
141
+ seats: input.seats,
142
+ status: 'available',
143
+ zone: input.zone,
144
+ zoneName: zone?.name,
145
+ shape: input.shape ?? 'square',
146
+ gridCol: input.gridCol ?? 0,
147
+ gridRow: input.gridRow ?? 0,
148
+ isActive: true,
149
+ tenantId: TENANT,
150
+ createdAt: new Date().toISOString(),
151
+ updatedAt: new Date().toISOString(),
152
+ }
153
+ tables.push(table)
154
+ return table
155
+ },
156
+
157
+ async updateTable(id: string, data: Partial<RestaurantTable>) {
158
+ const table = findTable(id)
159
+ if (!table) throw new Error(`Table ${id} not found`)
160
+ Object.assign(table, data, { updatedAt: new Date().toISOString() })
161
+ return table
162
+ },
163
+
164
+ async deleteTable(id: string) {
165
+ const idx = tables.findIndex(t => t.id === id)
166
+ if (idx !== -1) tables.splice(idx, 1)
167
+ },
168
+
169
+ async updateTableStatus(input: UpdateTableStatusInput) {
170
+ const table = findTable(input.tableId)
171
+ if (!table) throw new Error(`Table ${input.tableId} not found`)
172
+ table.status = input.status
173
+ table.updatedAt = new Date().toISOString()
174
+ return table
175
+ },
176
+
177
+ async updateTablePositions(positions: Array<{ id: string; gridCol: number; gridRow: number }>) {
178
+ for (const pos of positions) {
179
+ const table = findTable(pos.id)
180
+ if (table) {
181
+ table.gridCol = pos.gridCol
182
+ table.gridRow = pos.gridRow
183
+ table.updatedAt = new Date().toISOString()
184
+ }
185
+ }
186
+ },
187
+
188
+ // ---- Sessions ----
189
+
190
+ async seatGuests(input: SeatGuestsInput) {
191
+ const table = findTable(input.tableId)
192
+ if (!table) throw new Error(`Table ${input.tableId} not found`)
193
+
194
+ const session: TableSession = {
195
+ id: nextId('session'),
196
+ tableId: input.tableId,
197
+ tableName: table.name,
198
+ guests: input.guests,
199
+ waiterId: input.waiterId,
200
+ waiterName: input.waiterId ? `Waiter ${input.waiterId}` : undefined,
201
+ seatedAt: new Date().toISOString(),
202
+ status: 'active',
203
+ notes: input.notes,
204
+ tenantId: TENANT,
205
+ createdAt: new Date().toISOString(),
206
+ }
207
+ sessions.push(session)
208
+
209
+ // Denormalize onto table
210
+ table.status = 'occupied'
211
+ table.currentSessionId = session.id
212
+ table.currentGuests = input.guests
213
+ table.currentWaiterName = session.waiterName
214
+ table.currentElapsedMinutes = 0
215
+ table.currentTotal = 0
216
+ table.updatedAt = new Date().toISOString()
217
+
218
+ return session
219
+ },
220
+
221
+ async closeSession(sessionId: string) {
222
+ const session = sessions.find(s => s.id === sessionId)
223
+ if (!session) throw new Error(`Session ${sessionId} not found`)
224
+
225
+ session.status = 'closed'
226
+ session.closedAt = new Date().toISOString()
227
+
228
+ // Update table → cleaning
229
+ const table = findTable(session.tableId)
230
+ if (table) {
231
+ table.status = 'cleaning'
232
+ table.currentSessionId = undefined
233
+ table.currentOrderId = undefined
234
+ table.currentGuests = undefined
235
+ table.currentWaiterName = undefined
236
+ table.currentElapsedMinutes = undefined
237
+ table.currentTotal = undefined
238
+ table.updatedAt = new Date().toISOString()
239
+ }
240
+
241
+ return session
242
+ },
243
+
244
+ async getActiveSessions() {
245
+ return sessions.filter(s => s.status === 'active')
246
+ },
247
+
248
+ async getSessionHistory(tableId?: string) {
249
+ const closed = sessions.filter(s => s.status === 'closed')
250
+ if (tableId) return closed.filter(s => s.tableId === tableId)
251
+ return closed
252
+ },
253
+
254
+ // ---- Zones ----
255
+
256
+ async getZones() {
257
+ return [...zones]
258
+ },
259
+
260
+ async createZone(data: Partial<Zone>) {
261
+ const zone: Zone = {
262
+ id: nextId('zone'),
263
+ name: data.name ?? 'New Zone',
264
+ color: data.color,
265
+ sortOrder: data.sortOrder ?? zones.length,
266
+ isActive: data.isActive ?? true,
267
+ tenantId: TENANT,
268
+ createdAt: new Date().toISOString(),
269
+ }
270
+ zones.push(zone)
271
+ return zone
272
+ },
273
+
274
+ async updateZone(id: string, data: Partial<Zone>) {
275
+ const zone = zones.find(z => z.id === id)
276
+ if (!zone) throw new Error(`Zone ${id} not found`)
277
+ Object.assign(zone, data)
278
+ return zone
279
+ },
280
+
281
+ async deleteZone(id: string) {
282
+ const idx = zones.findIndex(z => z.id === id)
283
+ if (idx !== -1) zones.splice(idx, 1)
284
+ },
285
+
286
+ // ---- Summary ----
287
+
288
+ async getSummary(): Promise<TablesSummary> {
289
+ const available = tables.filter(t => t.status === 'available').length
290
+ const occupied = tables.filter(t => t.status === 'occupied').length
291
+ const reserved = tables.filter(t => t.status === 'reserved').length
292
+ const cleaning = tables.filter(t => t.status === 'cleaning').length
293
+
294
+ const totalSeats = tables.reduce((sum, t) => sum + t.seats, 0)
295
+ const occupiedSeats = tables
296
+ .filter(t => t.status === 'occupied')
297
+ .reduce((sum, t) => sum + (t.currentGuests ?? t.seats), 0)
298
+
299
+ const closedSessions = sessions.filter(s => s.status === 'closed' && s.closedAt)
300
+ let averageSessionMinutes = 0
301
+ if (closedSessions.length > 0) {
302
+ const totalMinutes = closedSessions.reduce((sum, s) => {
303
+ const seated = new Date(s.seatedAt).getTime()
304
+ const closed = new Date(s.closedAt!).getTime()
305
+ return sum + (closed - seated) / 60_000
306
+ }, 0)
307
+ averageSessionMinutes = Math.round(totalMinutes / closedSessions.length)
308
+ }
309
+
310
+ return {
311
+ totalTables: tables.length,
312
+ availableCount: available,
313
+ occupiedCount: occupied,
314
+ reservedCount: reserved,
315
+ cleaningCount: cleaning,
316
+ totalSeats,
317
+ occupiedSeats,
318
+ averageSessionMinutes,
319
+ }
320
+ },
321
+ }
322
+ }
@@ -0,0 +1,31 @@
1
+ import type {
2
+ RestaurantTable, TableSession, Zone,
3
+ CreateTableInput, SeatGuestsInput, UpdateTableStatusInput,
4
+ TableQuery, TablesSummary,
5
+ } from '../types'
6
+
7
+ export interface TablesDataProvider {
8
+ // Tables
9
+ getTables(query?: TableQuery): Promise<RestaurantTable[]>
10
+ getTableById(id: string): Promise<RestaurantTable | null>
11
+ createTable(input: CreateTableInput): Promise<RestaurantTable>
12
+ updateTable(id: string, data: Partial<RestaurantTable>): Promise<RestaurantTable>
13
+ deleteTable(id: string): Promise<void>
14
+ updateTableStatus(input: UpdateTableStatusInput): Promise<RestaurantTable>
15
+ updateTablePositions(positions: Array<{ id: string; gridCol: number; gridRow: number }>): Promise<void>
16
+
17
+ // Sessions
18
+ seatGuests(input: SeatGuestsInput): Promise<TableSession>
19
+ closeSession(sessionId: string): Promise<TableSession>
20
+ getActiveSessions(): Promise<TableSession[]>
21
+ getSessionHistory(tableId?: string): Promise<TableSession[]>
22
+
23
+ // Zones
24
+ getZones(): Promise<Zone[]>
25
+ createZone(data: Partial<Zone>): Promise<Zone>
26
+ updateZone(id: string, data: Partial<Zone>): Promise<Zone>
27
+ deleteZone(id: string): Promise<void>
28
+
29
+ // Summary
30
+ getSummary(): Promise<TablesSummary>
31
+ }