Adds a "Site data: X min ago" line derived from snapshot.snapshot_taken_at so managers can see how fresh the stats snapshot is. All other Part 5 items (menu-app api.js, OrderConfirm polling, manager-app api.js, IncomingOrdersPage 15s poll + OrderCard actions) were already fully implemented in the existing scaffolding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
117 lines
4.2 KiB
JavaScript
117 lines
4.2 KiB
JavaScript
import { useEffect, useState } from 'react'
|
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
import { ArrowLeft, RefreshCw, Bell } from 'lucide-react'
|
|
import { fetchSnapshot, fetchPendingOrders } from '../api'
|
|
import StatCard from '../components/StatCard'
|
|
import OrderCard from '../components/OrderCard'
|
|
|
|
const POLL_MS = 15_000
|
|
|
|
export default function DashboardPage() {
|
|
const { siteId } = useParams()
|
|
const navigate = useNavigate()
|
|
const [snapshot, setSnapshot] = useState(null)
|
|
const [pending, setPending] = useState([])
|
|
const [lastUpdated, setLastUpdated] = useState(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
async function load() {
|
|
try {
|
|
const [snap, orders] = await Promise.all([
|
|
fetchSnapshot(siteId).catch(() => null),
|
|
fetchPendingOrders(siteId).catch(() => []),
|
|
])
|
|
setSnapshot(snap ? JSON.parse(snap.snapshot_json) : null)
|
|
setPending(orders)
|
|
setLastUpdated(new Date())
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
load()
|
|
const timer = setInterval(load, POLL_MS)
|
|
return () => clearInterval(timer)
|
|
}, [siteId])
|
|
|
|
const stats = snapshot || {}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 pb-10">
|
|
{/* Header */}
|
|
<div className="bg-white border-b border-slate-100 shadow-sm sticky top-0 z-10">
|
|
<div className="max-w-2xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
<button onClick={() => navigate('/')} className="text-slate-400 hover:text-slate-700">
|
|
<ArrowLeft size={20} />
|
|
</button>
|
|
<h1 className="font-bold text-slate-800">Dashboard</h1>
|
|
<button onClick={load} className="text-slate-400 hover:text-slate-700">
|
|
<RefreshCw size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-2xl mx-auto px-4 py-5 space-y-5">
|
|
|
|
{/* Stats grid */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<StatCard label="Open tables" value={stats.open_tables} />
|
|
<StatCard label="Revenue today" value={stats.today_revenue != null ? `€${stats.today_revenue.toFixed(2)}` : null} color="text-emerald-600" />
|
|
<StatCard label="Orders today" value={stats.today_orders} />
|
|
<StatCard label="Online pending" value={stats.online_orders_pending} color={stats.online_orders_pending > 0 ? 'text-amber-600' : 'text-slate-800'} />
|
|
</div>
|
|
|
|
{lastUpdated && (
|
|
<p className="text-xs text-slate-400 text-right">
|
|
Refreshed {lastUpdated.toLocaleTimeString()}
|
|
</p>
|
|
)}
|
|
|
|
{stats.snapshot_taken_at && (() => {
|
|
const diffMin = Math.round((Date.now() - new Date(stats.snapshot_taken_at).getTime()) / 60_000)
|
|
const label = diffMin < 1 ? 'just now' : `${diffMin} min ago`
|
|
return (
|
|
<p className="text-xs text-slate-400 text-right -mt-3">
|
|
Site data: {label}
|
|
</p>
|
|
)
|
|
})()}
|
|
|
|
{/* Pending orders */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="font-bold text-slate-700 flex items-center gap-2">
|
|
<Bell size={16} className={pending.length ? 'text-amber-500' : 'text-slate-300'} />
|
|
Pending orders
|
|
{pending.length > 0 && (
|
|
<span className="bg-amber-100 text-amber-700 text-xs font-bold px-2 py-0.5 rounded-full">
|
|
{pending.length}
|
|
</span>
|
|
)}
|
|
</h2>
|
|
<button
|
|
onClick={() => navigate(`/${siteId}/orders`)}
|
|
className="text-sky-500 text-sm font-medium hover:underline"
|
|
>
|
|
View all
|
|
</button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-8">
|
|
<div className="w-6 h-6 rounded-full border-2 border-sky-400 border-t-transparent animate-spin" />
|
|
</div>
|
|
) : pending.length === 0 ? (
|
|
<div className="bg-white rounded-2xl p-8 text-center text-slate-400 text-sm shadow-sm">
|
|
No pending orders
|
|
</div>
|
|
) : (
|
|
pending.map(o => <OrderCard key={o.id} order={o} />)
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|