@fayz-ai/plugin-tables 0.9.0 → 0.9.2
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/README.md +4 -4
- package/dist/{TablesPage-IM732NWO.js → TablesPage-QUFTXYLX.js} +4 -4
- package/dist/TablesPage-QUFTXYLX.js.map +1 -0
- package/dist/{TablesContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/{TablesPage.d.ts → views/TablesPage.d.ts} +3 -3
- package/dist/views/TablesPage.d.ts.map +1 -0
- package/package.json +6 -8
- package/dist/TablesContext.d.ts.map +0 -1
- package/dist/TablesPage-IM732NWO.js.map +0 -1
- package/dist/TablesPage.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/TablesContext.tsx +0 -41
- package/src/TablesPage.tsx +0 -20
- package/src/data/mock.ts +0 -322
- package/src/data/platform.ts +0 -348
- package/src/data/types.ts +0 -31
- package/src/index.ts +0 -179
- package/src/locales/en.ts +0 -45
- package/src/locales/index.ts +0 -4
- package/src/locales/pt-BR.ts +0 -45
- package/src/registries.ts +0 -32
- package/src/store.ts +0 -178
- package/src/types.ts +0 -116
- package/src/views/FloorPlanView.tsx +0 -388
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
package/src/store.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { createStore, type StoreApi } from 'zustand/vanilla'
|
|
2
|
-
import { dedup } from '@fayz-ai/saas'
|
|
3
|
-
import { toast } from 'sonner'
|
|
4
|
-
import type { TablesDataProvider } from './data/types'
|
|
5
|
-
import type {
|
|
6
|
-
RestaurantTable, TableSession, Zone,
|
|
7
|
-
CreateTableInput, SeatGuestsInput, UpdateTableStatusInput,
|
|
8
|
-
TableQuery, TablesSummary,
|
|
9
|
-
} from './types'
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Store state
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
export interface TablesUIState {
|
|
16
|
-
// Data cache
|
|
17
|
-
tables: RestaurantTable[]
|
|
18
|
-
tablesLoading: boolean
|
|
19
|
-
|
|
20
|
-
zones: Zone[]
|
|
21
|
-
zonesLoading: boolean
|
|
22
|
-
|
|
23
|
-
activeSessions: TableSession[]
|
|
24
|
-
sessionsLoading: boolean
|
|
25
|
-
|
|
26
|
-
sessionHistory: TableSession[]
|
|
27
|
-
historyLoading: boolean
|
|
28
|
-
|
|
29
|
-
summary: TablesSummary | null
|
|
30
|
-
summaryLoading: boolean
|
|
31
|
-
|
|
32
|
-
selectedTableId: string | null
|
|
33
|
-
|
|
34
|
-
// Actions
|
|
35
|
-
fetchTables(query?: TableQuery): Promise<void>
|
|
36
|
-
fetchZones(): Promise<void>
|
|
37
|
-
fetchActiveSessions(): Promise<void>
|
|
38
|
-
fetchSessionHistory(tableId?: string): Promise<void>
|
|
39
|
-
fetchSummary(): Promise<void>
|
|
40
|
-
selectTable(id: string | null): void
|
|
41
|
-
seatGuests(input: SeatGuestsInput): Promise<TableSession>
|
|
42
|
-
closeSession(sessionId: string): Promise<void>
|
|
43
|
-
updateTableStatus(input: UpdateTableStatusInput): Promise<void>
|
|
44
|
-
createTable(input: CreateTableInput): Promise<RestaurantTable>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Store factory
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
export function createTablesStore(provider: TablesDataProvider): StoreApi<TablesUIState> {
|
|
52
|
-
return createStore<TablesUIState>((set, get) => ({
|
|
53
|
-
tables: [],
|
|
54
|
-
tablesLoading: false,
|
|
55
|
-
|
|
56
|
-
zones: [],
|
|
57
|
-
zonesLoading: false,
|
|
58
|
-
|
|
59
|
-
activeSessions: [],
|
|
60
|
-
sessionsLoading: false,
|
|
61
|
-
|
|
62
|
-
sessionHistory: [],
|
|
63
|
-
historyLoading: false,
|
|
64
|
-
|
|
65
|
-
summary: null,
|
|
66
|
-
summaryLoading: false,
|
|
67
|
-
|
|
68
|
-
selectedTableId: null,
|
|
69
|
-
|
|
70
|
-
async fetchTables(query) {
|
|
71
|
-
return dedup('tables:tables:' + JSON.stringify(query), async () => {
|
|
72
|
-
set({ tablesLoading: true })
|
|
73
|
-
const tables = await provider.getTables(query)
|
|
74
|
-
set({ tables, tablesLoading: false })
|
|
75
|
-
})
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
async fetchZones() {
|
|
79
|
-
return dedup('tables:zones', async () => {
|
|
80
|
-
set({ zonesLoading: true })
|
|
81
|
-
const zones = await provider.getZones()
|
|
82
|
-
set({ zones, zonesLoading: false })
|
|
83
|
-
})
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
async fetchActiveSessions() {
|
|
87
|
-
return dedup('tables:activeSessions', async () => {
|
|
88
|
-
set({ sessionsLoading: true })
|
|
89
|
-
const activeSessions = await provider.getActiveSessions()
|
|
90
|
-
set({ activeSessions, sessionsLoading: false })
|
|
91
|
-
})
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
async fetchSessionHistory(tableId) {
|
|
95
|
-
return dedup('tables:history:' + (tableId ?? ''), async () => {
|
|
96
|
-
set({ historyLoading: true })
|
|
97
|
-
const sessionHistory = await provider.getSessionHistory(tableId)
|
|
98
|
-
set({ sessionHistory, historyLoading: false })
|
|
99
|
-
})
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
async fetchSummary() {
|
|
103
|
-
return dedup('tables:summary', async () => {
|
|
104
|
-
set({ summaryLoading: true })
|
|
105
|
-
const summary = await provider.getSummary()
|
|
106
|
-
set({ summary, summaryLoading: false })
|
|
107
|
-
})
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
selectTable(id) {
|
|
111
|
-
set({ selectedTableId: id })
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
async seatGuests(input) {
|
|
115
|
-
try {
|
|
116
|
-
const session = await provider.seatGuests(input)
|
|
117
|
-
const [tables, activeSessions, summary] = await Promise.all([
|
|
118
|
-
provider.getTables(),
|
|
119
|
-
provider.getActiveSessions(),
|
|
120
|
-
provider.getSummary(),
|
|
121
|
-
])
|
|
122
|
-
set({ tables, activeSessions, summary })
|
|
123
|
-
toast.success('Guests seated')
|
|
124
|
-
return session
|
|
125
|
-
} catch (err: any) {
|
|
126
|
-
toast.error('Failed to seat guests', { description: err?.message })
|
|
127
|
-
throw err
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
async closeSession(sessionId) {
|
|
132
|
-
try {
|
|
133
|
-
await provider.closeSession(sessionId)
|
|
134
|
-
const [tables, activeSessions, summary] = await Promise.all([
|
|
135
|
-
provider.getTables(),
|
|
136
|
-
provider.getActiveSessions(),
|
|
137
|
-
provider.getSummary(),
|
|
138
|
-
])
|
|
139
|
-
set({ tables, activeSessions, summary })
|
|
140
|
-
toast.success('Session closed')
|
|
141
|
-
} catch (err: any) {
|
|
142
|
-
toast.error('Failed to close session', { description: err?.message })
|
|
143
|
-
throw err
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
async updateTableStatus(input) {
|
|
148
|
-
try {
|
|
149
|
-
await provider.updateTableStatus(input)
|
|
150
|
-
const [tables, summary] = await Promise.all([
|
|
151
|
-
provider.getTables(),
|
|
152
|
-
provider.getSummary(),
|
|
153
|
-
])
|
|
154
|
-
set({ tables, summary })
|
|
155
|
-
toast.success('Table status updated')
|
|
156
|
-
} catch (err: any) {
|
|
157
|
-
toast.error('Failed to update table status', { description: err?.message })
|
|
158
|
-
throw err
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
async createTable(input) {
|
|
163
|
-
try {
|
|
164
|
-
const table = await provider.createTable(input)
|
|
165
|
-
const [tables, summary] = await Promise.all([
|
|
166
|
-
provider.getTables(),
|
|
167
|
-
provider.getSummary(),
|
|
168
|
-
])
|
|
169
|
-
set({ tables, summary })
|
|
170
|
-
toast.success('Table created')
|
|
171
|
-
return table
|
|
172
|
-
} catch (err: any) {
|
|
173
|
-
toast.error('Failed to create table', { description: err?.message })
|
|
174
|
-
throw err
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
}))
|
|
178
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Tables Plugin — Pure TypeScript types
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
// ============================================================
|
|
6
|
-
// ENUMS / LITERALS
|
|
7
|
-
// ============================================================
|
|
8
|
-
|
|
9
|
-
export type TableStatus = 'available' | 'occupied' | 'reserved' | 'cleaning'
|
|
10
|
-
export type TableShape = 'square' | 'round' | 'rectangle' | 'bar'
|
|
11
|
-
|
|
12
|
-
// ============================================================
|
|
13
|
-
// CORE ENTITIES
|
|
14
|
-
// ============================================================
|
|
15
|
-
|
|
16
|
-
export interface RestaurantTable {
|
|
17
|
-
id: string
|
|
18
|
-
name: string
|
|
19
|
-
number: number
|
|
20
|
-
seats: number
|
|
21
|
-
status: TableStatus
|
|
22
|
-
zone: string
|
|
23
|
-
zoneName?: string
|
|
24
|
-
shape: TableShape
|
|
25
|
-
gridCol: number
|
|
26
|
-
gridRow: number
|
|
27
|
-
isActive: boolean
|
|
28
|
-
// Current session (denormalized for floor plan)
|
|
29
|
-
currentSessionId?: string
|
|
30
|
-
currentOrderId?: string
|
|
31
|
-
currentGuests?: number
|
|
32
|
-
currentWaiterName?: string
|
|
33
|
-
currentElapsedMinutes?: number
|
|
34
|
-
currentTotal?: number
|
|
35
|
-
metadata?: Record<string, unknown>
|
|
36
|
-
tenantId: string
|
|
37
|
-
createdAt: string
|
|
38
|
-
updatedAt: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface TableSession {
|
|
42
|
-
id: string
|
|
43
|
-
tableId: string
|
|
44
|
-
tableName?: string
|
|
45
|
-
orderId?: string
|
|
46
|
-
guests: number
|
|
47
|
-
waiterId?: string
|
|
48
|
-
waiterName?: string
|
|
49
|
-
seatedAt: string
|
|
50
|
-
closedAt?: string
|
|
51
|
-
status: 'active' | 'closed'
|
|
52
|
-
notes?: string
|
|
53
|
-
tenantId: string
|
|
54
|
-
createdAt: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface Zone {
|
|
58
|
-
id: string
|
|
59
|
-
name: string
|
|
60
|
-
color?: string
|
|
61
|
-
sortOrder: number
|
|
62
|
-
isActive: boolean
|
|
63
|
-
tenantId: string
|
|
64
|
-
createdAt: string
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ============================================================
|
|
68
|
-
// INPUT TYPES
|
|
69
|
-
// ============================================================
|
|
70
|
-
|
|
71
|
-
export interface CreateTableInput {
|
|
72
|
-
name: string
|
|
73
|
-
number: number
|
|
74
|
-
seats: number
|
|
75
|
-
zone: string
|
|
76
|
-
shape?: TableShape
|
|
77
|
-
gridCol?: number
|
|
78
|
-
gridRow?: number
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface SeatGuestsInput {
|
|
82
|
-
tableId: string
|
|
83
|
-
guests: number
|
|
84
|
-
waiterId?: string
|
|
85
|
-
notes?: string
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface UpdateTableStatusInput {
|
|
89
|
-
tableId: string
|
|
90
|
-
status: TableStatus
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ============================================================
|
|
94
|
-
// QUERY TYPES
|
|
95
|
-
// ============================================================
|
|
96
|
-
|
|
97
|
-
export interface TableQuery {
|
|
98
|
-
zone?: string
|
|
99
|
-
status?: TableStatus | TableStatus[]
|
|
100
|
-
search?: string
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ============================================================
|
|
104
|
-
// AGGREGATION
|
|
105
|
-
// ============================================================
|
|
106
|
-
|
|
107
|
-
export interface TablesSummary {
|
|
108
|
-
totalTables: number
|
|
109
|
-
availableCount: number
|
|
110
|
-
occupiedCount: number
|
|
111
|
-
reservedCount: number
|
|
112
|
-
cleaningCount: number
|
|
113
|
-
totalSeats: number
|
|
114
|
-
occupiedSeats: number
|
|
115
|
-
averageSessionMinutes: number
|
|
116
|
-
}
|
|
@@ -1,388 +0,0 @@
|
|
|
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
|
-
}
|
|
File without changes
|