feat(frontend): workday summary modal, print analytics UI, staff zone-PIN management, report upgrades

manager_dashboard:
- DashboardPage: WorkdaySummaryModal integration (view / close-day flows); revenue chart upgrades
- WorkdaySummaryModal: new component — shows full shift/revenue summary before closing the day
- StaffTab: full rewrite — zone-assignment modal, reset-PIN modal, actions dropdown, richer staff card
- ProductsTab: digital-menu fields surfaced in product form
- PrintFontsTab: expanded font-size/weight controls per printer channel
- TablesConfigTab / TablesPage / tableColourStore: color picker and zone fields for tables
- FilterBar: unified filter component with date-range, printer, and category selectors
- CategoryPerformance / ProductPerformance / TableAnalytics: donut/bar charts,
  thermal+browser print modals, richer breakdown tables
- PrinterHistory: redesigned with per-printer drill-down and summary blocks
- TrafficAnalytics / WorkDaySummary / RevenueTrends / ShiftsOverview / StaffLeaderboard: polish pass
- tokens.js: design token updates

waiter_pwa:
- TableCard: show table color indicator from zone/colour config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 20:28:23 +03:00
parent 79bd3b0f41
commit ac7ec45279
21 changed files with 3353 additions and 645 deletions

View File

@@ -44,7 +44,7 @@ export default function TablesPage() {
const [selected, setSelected] = useState(new Set())
const [anyHovered, setAnyHovered] = useState(false)
const dragGroupId = useRef(null)
const [dragOverId, setDragOverId] = useState(null)
const [dragOverGap, setDragOverGap] = useState(null) // index into groups array (0 = before first)
const { data: tables = [], isLoading } = useQuery({
queryKey: ['tables-all', showInactive],
@@ -166,43 +166,70 @@ export default function TablesPage() {
<div className="flex gap-0 flex-wrap border-b border-slate-200 px-4 flex-shrink-0">
{zoneTabs.map(tab => {
const isGroup = tab.id !== 'all' && tab.id !== 'ungrouped'
const isDragOver = dragOverId === tab.id
const groupIdx = isGroup ? groups.findIndex(g => g.id === tab.id) : -1
function handleDragOver(e) {
e.preventDefault()
// Use mouse position within the tab to decide left-half vs right-half gap
const rect = e.currentTarget.getBoundingClientRect()
const isLeftHalf = e.clientX < rect.left + rect.width / 2
setDragOverGap(isLeftHalf ? groupIdx : groupIdx + 1)
}
function handleDrop(e) {
e.preventDefault()
const fromId = dragGroupId.current
dragGroupId.current = null
setDragOverGap(null)
if (!fromId) return
const groupIds = groups.map(g => g.id)
const fromIdx = groupIds.indexOf(fromId)
if (fromIdx === -1 || dragOverGap === null) return
const reordered = [...groupIds]
reordered.splice(fromIdx, 1)
// After removing the dragged item, adjust target index
const insertAt = dragOverGap > fromIdx ? dragOverGap - 1 : dragOverGap
reordered.splice(insertAt, 0, fromId)
if (reordered.join() !== groupIds.join()) reorderGroups.mutate(reordered)
}
const showLineLeft = isGroup && dragOverGap === groupIdx
const showLineRight = isGroup && dragOverGap === groupIdx + 1 && groupIdx === groups.length - 1
return (
<button
key={tab.id}
onClick={() => { setActiveTab(tab.id); clearSelect() }}
draggable={isGroup}
onDragStart={isGroup ? () => { dragGroupId.current = tab.id } : undefined}
onDragOver={isGroup ? (e) => { e.preventDefault(); setDragOverId(tab.id) } : undefined}
onDragLeave={isGroup ? () => setDragOverId(null) : undefined}
onDrop={isGroup ? (e) => {
e.preventDefault()
setDragOverId(null)
const fromId = dragGroupId.current
if (!fromId || fromId === tab.id) return
const groupIds = groups.map(g => g.id)
const fromIdx = groupIds.indexOf(fromId)
const toIdx = groupIds.indexOf(tab.id)
if (fromIdx === -1 || toIdx === -1) return
const reordered = [...groupIds]
reordered.splice(fromIdx, 1)
reordered.splice(toIdx, 0, fromId)
reorderGroups.mutate(reordered)
} : undefined}
onDragEnd={isGroup ? () => { dragGroupId.current = null; setDragOverId(null) } : undefined}
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeTab === tab.id
? 'border-sky-500 text-sky-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
} ${isDragOver ? 'bg-sky-50 border-sky-300' : ''} ${isGroup ? 'cursor-grab active:cursor-grabbing' : ''}`}
>
{isGroup && <span className="text-slate-300 text-xs mr-0.5 select-none"></span>}
{tab.color && <span className="w-2 h-2 rounded-full shrink-0" style={{ background: tab.color }} />}
{tab.label}
<span className="ml-0.5 text-xs text-slate-400">
({tab.id === 'all' ? tables.length : tab.id === 'ungrouped' ? tables.filter(t => !t.group_id).length : tables.filter(t => t.group_id === tab.id).length})
</span>
</button>
<div key={tab.id} className="relative flex items-stretch">
{/* Drop indicator line — left side of this tab */}
{showLineLeft && (
<div className="absolute left-0 top-1 bottom-1 w-0.5 bg-sky-500 rounded-full z-10 -translate-x-px" />
)}
<button
onClick={() => { setActiveTab(tab.id); clearSelect() }}
draggable={isGroup}
onDragStart={isGroup ? () => { dragGroupId.current = tab.id } : undefined}
onDragOver={isGroup ? handleDragOver : undefined}
onDragLeave={isGroup ? () => setDragOverGap(null) : undefined}
onDrop={isGroup ? handleDrop : undefined}
onDragEnd={isGroup ? () => { dragGroupId.current = null; setDragOverGap(null) } : undefined}
className={`flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeTab === tab.id
? 'border-sky-500 text-sky-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
} ${isGroup ? 'cursor-grab active:cursor-grabbing' : ''}`}
>
{isGroup && <span className="text-slate-300 text-xs mr-0.5 select-none"></span>}
{tab.color && <span className="w-2 h-2 rounded-full shrink-0" style={{ background: tab.color }} />}
{tab.label}
<span className="ml-0.5 text-xs text-slate-400">
({tab.id === 'all' ? tables.length : tab.id === 'ungrouped' ? tables.filter(t => !t.group_id).length : tables.filter(t => t.group_id === tab.id).length})
</span>
</button>
{/* Drop indicator line — right side of the last group tab */}
{showLineRight && (
<div className="absolute right-0 top-1 bottom-1 w-0.5 bg-sky-500 rounded-full z-10 translate-x-px" />
)}
</div>
)
})}
</div>