@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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/TablesContext.d.ts +35 -0
- package/dist/TablesContext.d.ts.map +1 -0
- package/dist/TablesPage-5GCFGSX3.cjs +276 -0
- package/dist/TablesPage-5GCFGSX3.cjs.map +1 -0
- package/dist/TablesPage-IM732NWO.js +274 -0
- package/dist/TablesPage-IM732NWO.js.map +1 -0
- package/dist/TablesPage.d.ts +13 -0
- package/dist/TablesPage.d.ts.map +1 -0
- package/dist/data/fayz.d.ts +15 -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 +24 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +887 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +880 -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 +28 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/FloorPlanView.d.ts +3 -0
- package/dist/views/FloorPlanView.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/TablesContext.tsx +41 -0
- package/src/TablesPage.tsx +20 -0
- package/src/data/fayz.ts +348 -0
- package/src/data/mock.ts +322 -0
- package/src/data/types.ts +31 -0
- package/src/index.ts +179 -0
- package/src/locales/en.ts +45 -0
- package/src/locales/index.ts +4 -0
- package/src/locales/pt-BR.ts +45 -0
- package/src/registries.ts +32 -0
- package/src/store.ts +178 -0
- package/src/types.ts +116 -0
- package/src/views/FloorPlanView.tsx +388 -0
package/src/data/fayz.ts
ADDED
|
@@ -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
|
+
}
|
package/src/data/mock.ts
ADDED
|
@@ -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
|
+
}
|