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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user