<head> <meta charset="utf-8" /> <title>Depot Assistant - Tasks & Scan Control</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- Tailwind via CDN --> <script src="https://cdn.tailwindcss.com"></script> <!-- React 18 UMD builds --> <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> <!-- Babel so we can use JSX directly in the browser --> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> </head> <body class="min-h-screen bg-slate-950 text-slate-50"> <div id="root"></div> <script type="text/babel"> const { useState, useMemo } = React; /** Create some demo tasks for initial render. */ function createInitialTasks() { const now = new Date(); const iso = (offsetHours) => new Date(now.getTime() + offsetHours * 60 * 60 * 1000).toISOString(); return [ { id: "t-1", title: "Unload pallets – BRATISLAVA HUB", description: "Priority CMR #SK123456, ramp 3, call dispatcher if damaged.", assignee: "Truck SK-BA 904", status: "inProgress", priority: "high", dueDate: iso(2), createdAt: iso(-1), }, { id: "t-2", title: "Scan incoming shipment – WAREHOUSE A", description: "Link scans to route 42 / depot.pogytransport.eu/scan", assignee: "Jano – forklift", status: "open", priority: "medium", dueDate: iso(4), createdAt: iso(-2), }, { id: "t-3", title: "Confirm CMR signatures – export run", description: "Upload missing CMR photos to the system.", assignee: "Dispatcher", status: "open", priority: "critical", dueDate: iso(-3), createdAt: iso(-5), }, { id: "t-4", title: "Clean scan station – lane 2", description: "Short downtime, plan for next break.", assignee: "Team B", status: "done", priority: "low", dueDate: iso(-10), createdAt: iso(-12), }, ]; } /** Badge for priority. */ function TaskPriorityBadge({ priority }) { const labelMap = { low: "Low", medium: "Medium", high: "High", critical: "Critical", }; let classes = "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wide "; switch (priority) { case "low": classes += "bg-slate-500/10 text-slate-200 ring-1 ring-slate-500/40"; break; case "medium": classes += "bg-sky-500/10 text-sky-300 ring-1 ring-sky-500/40"; break; case "high": classes += "bg-amber-500/10 text-amber-300 ring-1 ring-amber-500/40"; break; case "critical": classes += "bg-rose-600/10 text-rose-300 ring-1 ring-rose-600/50"; break; default: classes += "bg-slate-500/10 text-slate-200 ring-1 ring-slate-500/40"; } return <span className={classes}>{labelMap[priority] || priority}</span>; } /** Badge for task status. */ function TaskStatusBadge({ status }) { const labelMap = { open: "Open", inProgress: "In progress", done: "Done", blocked: "Blocked", }; let classes = "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium "; switch (status) { case "open": classes += "bg-sky-500/10 text-sky-300 ring-1 ring-sky-500/40"; break; case "inProgress": classes += "bg-amber-500/10 text-amber-300 ring-1 ring-amber-500/40"; break; case "done": classes += "bg-emerald-500/10 text-emerald-300 ring-1 ring-emerald-500/40"; break; case "blocked": classes += "bg-rose-500/10 text-rose-300 ring-1 ring-rose-500/40"; break; default: classes += "bg-slate-500/10 text-slate-200 ring-1 ring-slate-500/40"; } return <span className={classes}>{labelMap[status] || status}</span>; } /** Filter bar. */ function TaskFilters({ status, search, onStatusChange, onSearchChange }) { return ( <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-center gap-2 text-xs text-slate-400"> <span className="hidden text-slate-500 sm:inline">Filter tasks:</span> <select value={status} onChange={(e) => onStatusChange(e.target.value)} className="h-9 rounded-lg border border-slate-700 bg-slate-900/80 px-2.5 text-xs text-slate-100 shadow-sm outline-none ring-sky-500/40 transition focus:border-sky-500 focus:ring-2" > <option value="all">All statuses</option> <option value="open">Open</option> <option value="inProgress">In progress</option> <option value="done">Done</option> <option value="blocked">Blocked</option> </select> </div> <div className="relative w-full sm:w-64"> <input value={search} onChange={(e) => onSearchChange(e.target.value)} placeholder="Search by title, assignee, ref…" className="h-9 w-full rounded-lg border border-slate-700 bg-slate-900/80 px-3 text-sm text-slate-100 shadow-sm outline-none ring-sky-500/40 transition placeholder:text-slate-500 focus:border-sky-500 focus:ring-2" /> </div> </div> ); } /** Inline form for creating tasks. */ function TaskFormInline({ onCreate }) { const [isOpen, setIsOpen] = useState(false); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [assignee, setAssignee] = useState(""); const [priority, setPriority] = useState("medium"); const [dueDate, setDueDate] = useState(""); const handleSubmit = (event) => { event.preventDefault(); if (!title.trim()) return; const payload = { title: title.trim(), description: description.trim() || undefined, assignee: assignee.trim() || "Unassigned", priority, dueDate: dueDate || undefined, }; onCreate(payload); setTitle(""); setDescription(""); setAssignee(""); setPriority("medium"); setDueDate(""); setIsOpen(false); }; if (!isOpen) { return ( <button type="button" onClick={() => setIsOpen(true)} className="inline-flex items-center gap-2 rounded-lg border border-dashed border-sky-500/60 bg-sky-500/5 px-3 py-2 text-xs font-medium uppercase tracking-wide text-sky-200 shadow-sm transition hover:border-sky-400 hover:bg-sky-500/10" > <span className="text-lg">+</span> New task </button> ); } return ( <form onSubmit={handleSubmit} className="space-y-3 rounded-xl border border-slate-800 bg-slate-950/70 p-4 shadow-sm" > <div className="flex items-center justify-between"> <p className="text-xs font-semibold uppercase tracking-wide text-slate-400"> Create task </p> <button type="button" onClick={() => setIsOpen(false)} className="text-xs font-medium text-slate-400 underline-offset-2 hover:text-slate-100 hover:underline" > Cancel </button> </div> <div className="grid gap-3 md:grid-cols-2"> <div className="space-y-1"> <label className="text-xs font-medium text-slate-200">Title</label> <input required value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Unload pallet – BRATISLAVA HUB" className="h-9 w-full rounded-md border border-slate-700 bg-slate-900/80 px-3 text-sm text-slate-100 outline-none ring-sky-500/40 transition placeholder:text-slate-500 focus:border-sky-500 focus:ring-2" /> </div> <div className="space-y-1"> <label className="text-xs font-medium text-slate-200"> Assignee / driver </label> <input value={assignee} onChange={(e) => setAssignee(e.target.value)} placeholder="Driver name or truck code" className="h-9 w-full rounded-md border border-slate-700 bg-slate-900/80 px-3 text-sm text-slate-100 outline-none ring-sky-500/40 transition placeholder:text-slate-500 focus:border-sky-500 focus:ring-2" /> </div> </div> <div className="space-y-1"> <label className="text-xs font-medium text-slate-200"> Description (optional) </label> <textarea rows={2} value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Notes for dispatcher or driver…" className="w-full rounded-md border border-slate-700 bg-slate-900/80 px-3 py-2 text-sm text-slate-100 outline-none ring-sky-500/40 transition placeholder:text-slate-500 focus:border-sky-500 focus:ring-2" /> </div> <div className="grid gap-3 md:grid-cols-[1fr_1fr_auto]"> <div className="space-y-1"> <label className="text-xs font-medium text-slate-200">Priority</label> <select value={priority} onChange={(e) => setPriority(e.target.value)} className="h-9 w-full rounded-md border border-slate-700 bg-slate-900/80 px-2.5 text-xs text-slate-100 outline-none ring-sky-500/40 transition focus:border-sky-500 focus:ring-2" > <option value="low">Low</option> <option value="medium">Medium</option> <option value="high">High</option> <option value="critical">Critical</option> </select> </div> <div className="space-y-1"> <label className="text-xs font-medium text-slate-200">Due date</label> <input type="date" value={dueDate} onChange={(e) => setDueDate(e.target.value)} className="h-9 w-full rounded-md border border-slate-700 bg-slate-900/80 px-3 text-sm text-slate-100 outline-none ring-sky-500/40 transition focus:border-sky-500 focus:ring-2" /> </div> <div className="flex items-end justify-end"> <button type="submit" className="inline-flex h-9 items-center gap-2 rounded-md bg-sky-500 px-4 text-xs font-semibold uppercase tracking-wide text-white shadow-sm transition hover:bg-sky-400" > <span className="text-lg">+</span> Save task </button> </div> </div> </form> ); } /** Stats from tasks. */ function computeStats(tasks) { const now = new Date(); let open = 0; let inProgress = 0; let done = 0; let blocked = 0; let overdue = 0; for (const task of tasks) { if (task.status === "open") open++; if (task.status === "inProgress") inProgress++; if (task.status === "done") done++; if (task.status === "blocked") blocked++; if (task.dueDate && task.status !== "done") { const due = new Date(task.dueDate); if (due.getTime() < now.getTime()) { overdue++; } } } return { open, inProgress, done, blocked, overdue }; } function TaskStats({ tasks }) { const stats = computeStats(tasks); return ( <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 shadow-sm"> <div className="flex items-center justify-between gap-2"> <div> <p className="text-xs font-medium uppercase tracking-wide text-slate-400"> Open tasks </p> <p className="mt-1 text-2xl font-semibold text-slate-50"> {stats.open} </p> </div> <div className="rounded-full bg-sky-500/10 px-3 py-2 text-sky-300 text-xl"> ✓ </div> </div> </div> <div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 shadow-sm"> <div className="flex items-center justify-between gap-2"> <div> <p className="text-xs font-medium uppercase tracking-wide text-slate-400"> In progress </p> <p className="mt-1 text-2xl font-semibold text-slate-50"> {stats.inProgress} </p> </div> <div className="rounded-full bg-amber-500/10 px-3 py-2 text-amber-300 text-xl"> … </div> </div> </div> <div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 shadow-sm"> <div className="flex items-center justify-between gap-2"> <div> <p className="text-xs font-medium uppercase tracking-wide text-slate-400"> Completed </p> <p className="mt-1 text-2xl font-semibold text-emerald-300"> {stats.done} </p> </div> <div className="rounded-full bg-emerald-500/10 px-3 py-2 text-emerald-300 text-xl"> ✔ </div> </div> </div> <div className="rounded-xl border border-slate-800 bg-slate-900/60 p-4 shadow-sm"> <div className="flex items-center justify-between gap-2"> <div> <p className="text-xs font-medium uppercase tracking-wide text-slate-400"> Overdue / blocked </p> <p className="mt-1 text-2xl font-semibold text-rose-300"> {stats.overdue + stats.blocked} </p> </div> <div className="rounded-full bg-rose-500/10 px-3 py-2 text-rose-300 text-xl"> ! </div> </div> </div> </div> ); } /** List of tasks. */ function TaskList({ tasks, onChangeStatus }) { if (tasks.length === 0) { return ( <div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-700 bg-slate-900/40 px-6 py-10 text-center"> <p className="text-sm font-medium text-slate-200">No tasks yet.</p> <p className="mt-1 text-xs text-slate-500"> Use the form above to create your first dispatcher task. </p> </div> ); } return ( <div className="overflow-hidden rounded-xl border border-slate-800 bg-slate-950/70"> <div className="hidden grid-cols-[minmax(0,3fr)_minmax(0,1.5fr)_minmax(0,1.5fr)_minmax(0,1.5fr)_minmax(0,1.5fr)] bg-slate-900/70 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-slate-400 md:grid"> <div>Task</div> <div>Assignee</div> <div>Priority</div> <div>Status</div> <div className="text-right">Due date</div> </div> <ul className="divide-y divide-slate-800"> {tasks.map((task) => ( <li key={task.id} className="grid grid-cols-1 gap-3 px-4 py-3 text-sm text-slate-100 md:grid-cols-[minmax(0,3fr)_minmax(0,1.5fr)_minmax(0,1.5fr)_minmax(0,1.5fr)_minmax(0,1.5fr)] md:items-center" > <div> <p className="font-medium">{task.title}</p> {task.description ? ( <p className="mt-0.5 text-xs text-slate-400"> {task.description} </p> ) : null} <p className="mt-1 text-[11px] uppercase tracking-wide text-slate-500"> Created {new Date(task.createdAt).toLocaleString()} </p> </div> <div className="flex items-center justify-between gap-2 md:block"> <p className="text-sm text-slate-100">{task.assignee}</p> <p className="text-[11px] uppercase tracking-wide text-slate-500 md:mt-1"> Assignee </p> </div> <div className="flex flex-col gap-1 md:items-start"> <TaskPriorityBadge priority={task.priority} /> <p className="text-[11px] uppercase tracking-wide text-slate-500"> Priority </p> </div> <div className="flex flex-col gap-1 md:items-start"> <TaskStatusBadge status={task.status} /> <div className="mt-1 flex gap-1 text-[11px]"> {task.status !== "done" && ( <button type="button" onClick={() => onChangeStatus(task.id, "done")} className="rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-emerald-300 transition hover:border-emerald-400 hover:bg-emerald-500/20" > Mark done </button> )} {task.status !== "open" && ( <button type="button" onClick={() => onChangeStatus(task.id, "open")} className="rounded-full border border-slate-500/40 bg-slate-800/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-slate-200 transition hover:border-slate-300 hover:bg-slate-700" > Reopen </button> )} </div> </div> <div className="flex flex-col items-end gap-1 text-right md:items-end"> <p className="text-sm text-slate-100"> {task.dueDate ? new Date(task.dueDate).toLocaleDateString() : "No due date"} </p> <p className="text-[11px] uppercase tracking-wide text-slate-500"> Due </p> </div> </li> ))} </ul> </div> ); } /** Basic app shell with header and content. */ function AppShell({ title, children }) { return ( <div className="min-h-screen bg-slate-950 text-slate-50"> <header className="border-b border-slate-900 bg-slate-950/80 backdrop-blur"> <div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3"> <div className="flex items-center gap-2"> <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-sky-500/20 text-sky-300 text-lg"> ✓ </div> <div className="leading-tight"> <p className="text-sm font-semibold">Depot Assistant</p> <p className="text-[11px] text-slate-400">Tasks & scan control center</p> </div> </div> <div className="hidden text-xs font-medium text-slate-400 sm:block"> <span className="rounded-full border border-slate-800 bg-slate-900/70 px-3 py-1"> Pilot dashboard </span> </div> </div> </header> <main className="mx-auto max-w-6xl px-4 py-6"> {title ? ( <div className="mb-4 flex flex-col gap-1 sm:mb-6"> <h1 className="text-xl font-semibold text-slate-50 sm:text-2xl"> {title} </h1> <p className="text-xs text-slate-400"> Quick overview of depot tasks connected to your scanning workflow. </p> </div> ) : null} {children} </main> </div> ); } /** Main home page component. */ function HomePage() { const [tasks, setTasks] = useState(() => createInitialTasks()); const [statusFilter, setStatusFilter] = useState("all"); const [search, setSearch] = useState(""); const filteredTasks = useMemo(() => { return tasks.filter((task) => { if (statusFilter !== "all" && task.status !== statusFilter) { return false; } if (!search.trim()) return true; const haystack = ( task.title + " " + (task.description || "") + " " + task.assignee ).toLowerCase(); const needle = search.trim().toLowerCase(); return haystack.includes(needle); }); }, [tasks, statusFilter, search]); const handleCreateTask = (input) => { const now = new Date().toISOString(); const newTask = { id: "t-" + Date.now(), title: input.title, description: input.description, assignee: input.assignee, status: "open", priority: input.priority, dueDate: input.dueDate, createdAt: now, }; setTasks((prev) => [newTask, ...prev]); }; const handleChangeStatus = (id, status) => { setTasks((prev) => prev.map((task) => (task.id === id ? { ...task, status } : task)) ); }; return ( <AppShell title="Depot tasks overview"> <section className="space-y-6"> <TaskStats tasks={tasks} /> <div className="mt-2 flex flex-col gap-4 rounded-2xl border border-slate-900 bg-slate-950/80 p-4 shadow-xl shadow-slate-950/40 sm:mt-4 sm:p-5"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div> <p className="text-sm font-semibold text-slate-50">Task list</p> <p className="text-xs text-slate-400"> Create tasks to connect scanning activities with clear responsibilities. </p> </div> <TaskFormInline onCreate={handleCreateTask} /> </div> <TaskFilters status={statusFilter} search={search} onStatusChange={setStatusFilter} onSearchChange={setSearch} /> <TaskList tasks={filteredTasks} onChangeStatus={handleChangeStatus} /> </div> </section> </AppShell> ); } function App() { return <HomePage />; } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />); </script> </body> </html>