50 KiB
import React, { useMemo, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Separator } from "@/components/ui/separator"; import { AlertTriangle, CheckCircle2, Clock, MapPin, TrendingUp, Building2, Wrench, Search, Filter, ChevronRight, ShieldAlert, Truck, Lightbulb, } from "lucide-react"; import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, BarChart, Bar, } from "recharts";
// --- Mock data (replace with API later) --- const DISTRICTS = ["All districts", "Mitte", "Nord", "Süd", "West", "Ost"]; const CATEGORIES = [ "All categories", "Waste", "Roads", "Lighting", "Noise", "Parking", "Water", ];
const kpisBase = { openedToday: 184, closedToday: 159, backlog: 1328, slaBreaches: 42, repeatIncidents: 61, medianResolveHours: 38, citizenSatisfaction: 4.1, };
const trendResolve = [ { week: "W-3", hours: 44 }, { week: "W-2", hours: 41 }, { week: "W-1", hours: 39 }, { week: "This", hours: 38 }, ];
const backlogAging = [ { bucket: "0–2d", count: 424 }, { bucket: "3–7d", count: 366 }, { bucket: "8–30d", count: 401 }, { bucket: "30d+", count: 137 }, ];
const hotspots = [ { id: "HS-101", district: "Nord", category: "Waste", title: "Overflowing bins near transit stop", location: "Rosenweg 12", ageDays: 6, priority: "High", repeats: 3, }, { id: "HS-102", district: "Mitte", category: "Lighting", title: "Streetlights out on main crossing", location: "Alexanderplatz East", ageDays: 2, priority: "Critical", repeats: 1, }, { id: "HS-103", district: "Süd", category: "Roads", title: "Potholes expanding after rain", location: "Bergstraße 7–15", ageDays: 11, priority: "High", repeats: 2, }, { id: "HS-104", district: "West", category: "Parking", title: "Illegal parking blocking fire lane", location: "Kantstraße 22", ageDays: 1, priority: "Medium", repeats: 4, }, ];
const demandSupply = [ { district: "Nord", category: "Waste", demandScore: 86, supplyScore: 42, gap: 44, note: "Contractors concentrated in Mitte; long travel times", }, { district: "Süd", category: "Roads", demandScore: 74, supplyScore: 51, gap: 23, note: "Asphalt crew scheduled; permitting delays", }, { district: "Mitte", category: "Lighting", demandScore: 61, supplyScore: 68, gap: -7, note: "Healthy coverage; parts inventory is bottleneck", }, { district: "West", category: "Parking", demandScore: 70, supplyScore: 33, gap: 37, note: "Enforcement staff shortage; peak weekend loads", }, ];
const providers = [ { id: "P-201", name: "NordClean GmbH", category: "Waste", district: "Nord", license: "Verified", etaHours: 6, rating: 4.6, capacity: "High", }, { id: "P-202", name: "CityLight Service", category: "Lighting", district: "Mitte", license: "Verified", etaHours: 10, rating: 4.3, capacity: "Medium", }, { id: "P-203", name: "WestPark Patrol", category: "Parking", district: "West", license: "Pending", etaHours: 12, rating: 4.0, capacity: "Low", }, { id: "P-204", name: "Bergstraße Asphalt", category: "Roads", district: "Süd", license: "Verified", etaHours: 18, rating: 4.4, capacity: "Medium", }, ];
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
function priorityBadge(priority) { const base = "rounded-full"; if (priority === "Critical") return {priority}; if (priority === "High") return {priority}; return {priority}; }
function formatHours(h) {
if (h < 24) return ${h}h;
const d = Math.round((h / 24) * 10) / 10;
return ${d}d;
}
function SmallKPI({ icon: Icon, label, value, hint, tone = "default" }) { const toneClass = tone === "danger" ? "border-destructive/30" : tone === "good" ? "border-emerald-500/20" : "border-border";
return (
<Card className={shadow-sm ${toneClass}}>
function RowBar({ label, left, right }) { const total = Math.max(1, left + right); const leftPct = (left / total) * 100; return (
${clamp(leftPct, 2, 98)}% }}
/>
export default function TurashCityDashboardPrototype() { const [district, setDistrict] = useState("All districts"); const [category, setCategory] = useState("All categories"); const [query, setQuery] = useState(""); const [publicMode, setPublicMode] = useState(false); const [selectedHotspot, setSelectedHotspot] = useState(null); const [budgetBoost, setBudgetBoost] = useState(12); // percent
const filteredHotspots = useMemo(() => { return hotspots .filter((h) => (district === "All districts" ? true : h.district === district)) .filter((h) => (category === "All categories" ? true : h.category === category)) .filter((h) => { if (!query.trim()) return true; const q = query.toLowerCase(); return ( h.title.toLowerCase().includes(q) || h.location.toLowerCase().includes(q) || h.id.toLowerCase().includes(q) ); }); }, [district, category, query]);
const suggestedProviders = useMemo(() => { if (!selectedHotspot) return []; const c = selectedHotspot.category; const d = selectedHotspot.district; return providers .filter((p) => p.category === c) .sort((a, b) => { const aMatch = a.district === d ? 0 : 1; const bMatch = b.district === d ? 0 : 1; if (aMatch !== bMatch) return aMatch - bMatch; return a.etaHours - b.etaHours; }) .slice(0, 4); }, [selectedHotspot]);
const simulated = useMemo(() => { // ultra-simplified “scenario”: more budget reduces median resolution and SLA breaches const factor = budgetBoost / 100; const median = Math.round(kpisBase.medianResolveHours (1 - 0.35 factor)); const breaches = Math.round(kpisBase.slaBreaches (1 - 0.55 factor)); const satisfaction = Math.round((kpisBase.citizenSatisfaction + 0.35 factor) 10) / 10; return { medianResolveHours: clamp(median, 18, 60), slaBreaches: clamp(breaches, 8, 80), citizenSatisfaction: clamp(satisfaction, 3.2, 4.8), }; }, [budgetBoost]);
return (
Turash — City Management Cockpit
PrototypeOperational awareness + vendor routing + economic insight (one thin layer, no 12‑month IT project).
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="flex items-center gap-2 rounded-2xl border bg-background px-3 py-2 shadow-sm">
<div className="text-xs text-muted-foreground">Public dashboard</div>
<Switch checked={publicMode} onCheckedChange={setPublicMode} />
</div>
<Button variant="secondary" className="rounded-2xl">
Export briefing
</Button>
<Button className="rounded-2xl">Create work order</Button>
</div>
</div>
{/* Filters */}
<div className="mt-5 grid grid-cols-1 gap-3 md:grid-cols-12">
<Card className="md:col-span-12 shadow-sm">
<CardContent className="p-4">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex flex-1 flex-col gap-3 md:flex-row md:items-center">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Filter className="h-4 w-4" />
<span>Filters</span>
</div>
<div className="grid flex-1 grid-cols-1 gap-3 sm:grid-cols-2">
<Select value={district} onValueChange={setDistrict}>
<SelectTrigger className="rounded-2xl">
<SelectValue placeholder="District" />
</SelectTrigger>
<SelectContent>
{DISTRICTS.map((d) => (
<SelectItem key={d} value={d}>{d}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={category} onValueChange={setCategory}>
<SelectTrigger className="rounded-2xl">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
{CATEGORIES.map((c) => (
<SelectItem key={c} value={c}>{c}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex w-full items-center gap-2 md:w-[360px]">
<div className="relative w-full">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search issue id, location, title…"
className="rounded-2xl pl-9"
/>
</div>
</div>
</div>
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
<Clock className="h-3.5 w-3.5" /> Last updated: just now (mock)
</span>
<Separator orientation="vertical" className="h-4" />
<span className="inline-flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5" /> Sources: 311 intake, permits, vendor registry
</span>
<Separator orientation="vertical" className="h-4" />
<span className="inline-flex items-center gap-1">
<ShieldAlert className="h-3.5 w-3.5" /> Data quality: medium (2 integrations pending)
</span>
</div>
</CardContent>
</Card>
</div>
{/* KPIs */}
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<SmallKPI
icon={TrendingUp}
label="Opened today"
value={kpisBase.openedToday}
hint="Citizen + sensor intake"
/>
<SmallKPI
icon={CheckCircle2}
label="Closed today"
value={kpisBase.closedToday}
hint="Includes contractor completions"
tone="good"
/>
<SmallKPI
icon={Clock}
label="Median resolve time"
value={`${simulated.medianResolveHours}h`}
hint={`Scenario: +${budgetBoost}% ops budget`}
/>
<SmallKPI
icon={AlertTriangle}
label="SLA breaches"
value={simulated.slaBreaches}
hint="Requires escalation"
tone="danger"
/>
</div>
{/* Tabs */}
<div className="mt-4">
<Tabs defaultValue="pulse" className="w-full">
<TabsList className="rounded-2xl">
<TabsTrigger value="pulse" className="rounded-xl">Pulse</TabsTrigger>
<TabsTrigger value="service" className="rounded-xl">Service performance</TabsTrigger>
<TabsTrigger value="economy" className="rounded-xl">Economy & supply</TabsTrigger>
</TabsList>
{/* PULSE */}
<TabsContent value="pulse" className="mt-4">
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
<Card className="lg:col-span-7 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Incident hotspots (map)</CardTitle>
<CardDescription>
Heatmap + what changed vs last week. Click a hotspot to route work.
</CardDescription>
</CardHeader>
<CardContent>
<div className="relative overflow-hidden rounded-2xl border bg-muted">
<div className="absolute inset-0 opacity-60" />
<div className="p-4">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">Map placeholder</div>
<Badge variant="outline" className="rounded-full inline-flex items-center gap-1">
<MapPin className="h-3.5 w-3.5" /> {district}
</Badge>
</div>
<div className="mt-3 grid grid-cols-2 gap-2 md:grid-cols-4">
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Heat</div>
<div className="text-lg font-semibold">↑ 12%</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">New clusters</div>
<div className="text-lg font-semibold">3</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Critical</div>
<div className="text-lg font-semibold">7</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Repeat sites</div>
<div className="text-lg font-semibold">{kpisBase.repeatIncidents}</div>
</div>
</div>
<div className="mt-4 text-xs text-muted-foreground">
In real deployments, this panel uses your GIS tile server or a lightweight map SDK.
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="lg:col-span-5 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Top hotspots</CardTitle>
<CardDescription>Sorted by priority × age × repeats</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{filteredHotspots.map((h) => (
<button
key={h.id}
onClick={() => setSelectedHotspot(h)}
className="w-full rounded-2xl border bg-background p-3 text-left shadow-sm transition hover:shadow"
>
<div className="flex items-start justify-between gap-3">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<div className="text-sm font-medium">{h.title}</div>
{priorityBadge(h.priority)}
</div>
<div className="text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1"><MapPin className="h-3.5 w-3.5" /> {h.location}</span>
<span className="mx-2">•</span>
<span>{h.district}</span>
<span className="mx-2">•</span>
<span>{h.category}</span>
</div>
</div>
<ChevronRight className="mt-1 h-4 w-4 text-muted-foreground" />
</div>
<div className="mt-3 grid grid-cols-3 gap-2">
<div className="rounded-xl border bg-muted/30 px-2 py-1">
<div className="text-[10px] uppercase text-muted-foreground">Age</div>
<div className="text-sm font-semibold">{h.ageDays}d</div>
</div>
<div className="rounded-xl border bg-muted/30 px-2 py-1">
<div className="text-[10px] uppercase text-muted-foreground">Repeats</div>
<div className="text-sm font-semibold">{h.repeats}</div>
</div>
<div className="rounded-xl border bg-muted/30 px-2 py-1">
<div className="text-[10px] uppercase text-muted-foreground">ID</div>
<div className="text-sm font-semibold">{h.id}</div>
</div>
</div>
</button>
))}
{filteredHotspots.length === 0 ? (
<div className="rounded-2xl border p-6 text-center text-sm text-muted-foreground">
No hotspots match the current filters.
</div>
) : null}
</div>
</CardContent>
</Card>
</div>
{/* Side panel */}
<AnimatePresence>
{selectedHotspot ? (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 12 }}
className="mt-3"
>
<Card className="shadow-sm">
<CardHeader>
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<CardTitle className="text-base">Selected hotspot</CardTitle>
<CardDescription>
Route work to local providers; track completion + citizen satisfaction.
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
className="rounded-2xl"
onClick={() => setSelectedHotspot(null)}
>
Close
</Button>
<Button className="rounded-2xl">Dispatch</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
<div className="lg:col-span-7">
<div className="rounded-2xl border bg-background p-4">
<div className="flex flex-wrap items-center gap-2">
<div className="text-lg font-semibold tracking-tight">{selectedHotspot.title}</div>
{priorityBadge(selectedHotspot.priority)}
<Badge variant="outline" className="rounded-full">{selectedHotspot.id}</Badge>
</div>
<div className="mt-2 grid grid-cols-1 gap-2 md:grid-cols-3">
<div className="rounded-xl border bg-muted/30 p-3">
<div className="text-xs text-muted-foreground">Location</div>
<div className="mt-1 text-sm font-medium inline-flex items-center gap-1">
<MapPin className="h-4 w-4" /> {selectedHotspot.location}
</div>
</div>
<div className="rounded-xl border bg-muted/30 p-3">
<div className="text-xs text-muted-foreground">District</div>
<div className="mt-1 text-sm font-medium">{selectedHotspot.district}</div>
</div>
<div className="rounded-xl border bg-muted/30 p-3">
<div className="text-xs text-muted-foreground">Category</div>
<div className="mt-1 text-sm font-medium">{selectedHotspot.category}</div>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-2 md:grid-cols-4">
<div className="rounded-xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Age</div>
<div className="mt-1 text-sm font-semibold">{selectedHotspot.ageDays} days</div>
</div>
<div className="rounded-xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Repeats</div>
<div className="mt-1 text-sm font-semibold">{selectedHotspot.repeats}</div>
</div>
<div className="rounded-xl border bg-background p-3">
<div className="text-xs text-muted-foreground">SLA</div>
<div className="mt-1 text-sm font-semibold">{selectedHotspot.priority === "Critical" ? "24h" : "72h"}</div>
</div>
<div className="rounded-xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Escalation</div>
<div className="mt-1 text-sm font-semibold">{selectedHotspot.ageDays > 7 ? "Yes" : "No"}</div>
</div>
</div>
<div className="mt-4 rounded-2xl border bg-muted/20 p-4">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4" />
<div>
<div className="text-sm font-medium">Root cause hint</div>
<div className="text-xs text-muted-foreground mt-1">
This is the kind of “repeat incident” flag city leadership reacts to: same site keeps resurfacing.
Turash can propose a systemic fix and show cost-to-serve if left unresolved.
</div>
</div>
</div>
</div>
</div>
</div>
<div className="lg:col-span-5">
<div className="rounded-2xl border bg-background p-4">
<div className="flex items-center justify-between">
<div className="text-sm font-semibold">Suggested providers</div>
<Badge variant="outline" className="rounded-full inline-flex items-center gap-1">
<Building2 className="h-3.5 w-3.5" /> Local-first
</Badge>
</div>
<div className="mt-3 space-y-2">
{suggestedProviders.map((p) => (
<div key={p.id} className="rounded-2xl border bg-muted/20 p-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-medium">{p.name}</div>
<div className="mt-1 text-xs text-muted-foreground">
{p.category} • {p.district} • {p.license}
</div>
</div>
<Badge className="rounded-full">ETA {formatHours(p.etaHours)}</Badge>
</div>
<div className="mt-2 grid grid-cols-3 gap-2 text-xs">
<div className="rounded-xl border bg-background px-2 py-1">
<div className="text-[10px] uppercase text-muted-foreground">Rating</div>
<div className="font-semibold">{p.rating}</div>
</div>
<div className="rounded-xl border bg-background px-2 py-1">
<div className="text-[10px] uppercase text-muted-foreground">Capacity</div>
<div className="font-semibold">{p.capacity}</div>
</div>
<div className="rounded-xl border bg-background px-2 py-1">
<div className="text-[10px] uppercase text-muted-foreground">ID</div>
<div className="font-semibold">{p.id}</div>
</div>
</div>
<div className="mt-3 flex gap-2">
<Button size="sm" className="rounded-2xl">Assign</Button>
<Button size="sm" variant="secondary" className="rounded-2xl">Broadcast</Button>
</div>
</div>
))}
{suggestedProviders.length === 0 ? (
<div className="rounded-2xl border p-4 text-sm text-muted-foreground">
No matching providers found. (This is a procurement-gap signal.)
</div>
) : null}
</div>
<div className="mt-4 rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Micro-procurement mode</div>
<div className="mt-1 text-sm">
Turn small jobs into fast routing — without replacing the city’s procurement system.
</div>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
) : null}
</AnimatePresence>
</TabsContent>
{/* SERVICE PERFORMANCE */}
<TabsContent value="service" className="mt-4">
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
<Card className="lg:col-span-7 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Median resolution time (4 weeks)</CardTitle>
<CardDescription>What leadership cares about: trend direction + exceptions</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={trendResolve} margin={{ top: 10, right: 12, bottom: 0, left: -10 }}>
<XAxis dataKey="week" tickLine={false} axisLine={false} />
<YAxis tickLine={false} axisLine={false} />
<Tooltip />
<Line type="monotone" dataKey="hours" stroke="currentColor" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="lg:col-span-5 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Backlog aging</CardTitle>
<CardDescription>Where the system is silently failing</CardDescription>
</CardHeader>
<CardContent className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={backlogAging} margin={{ top: 10, right: 12, bottom: 0, left: -10 }}>
<XAxis dataKey="bucket" tickLine={false} axisLine={false} />
<YAxis tickLine={false} axisLine={false} />
<Tooltip />
<Bar dataKey="count" fill="currentColor" radius={[12, 12, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<div className="mt-3 grid grid-cols-1 gap-3 lg:grid-cols-12">
<Card className="lg:col-span-7 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Department performance (example)</CardTitle>
<CardDescription>Simple, comparable, action-driving metrics</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<RowBar label="Waste ops (closed / opened)" left={63} right={78} />
<RowBar label="Road maintenance (closed / opened)" left={44} right={61} />
<RowBar label="Lighting (closed / opened)" left={36} right={29} />
<RowBar label="Parking enforcement (closed / opened)" left={22} right={48} />
<div className="rounded-2xl border bg-muted/20 p-4 text-sm">
<div className="flex items-start gap-2">
<ShieldAlert className="mt-0.5 h-4 w-4" />
<div>
<div className="font-medium">Escalation rule (example)</div>
<div className="mt-1 text-xs text-muted-foreground">
If backlog 30d+ grows for 2 consecutive weeks → auto-create an escalation brief with cost & hotspot list.
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="lg:col-span-5 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Scenario slider</CardTitle>
<CardDescription>Budget → measurable outcomes (simple, honest)</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium">Ops budget increase</div>
<Badge className="rounded-full">+{budgetBoost}%</Badge>
</div>
<input
className="w-full"
type="range"
min={0}
max={30}
value={budgetBoost}
onChange={(e) => setBudgetBoost(parseInt(e.target.value, 10))}
/>
<div className="grid grid-cols-1 gap-2">
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Median resolve time</div>
<div className="mt-1 text-lg font-semibold">{simulated.medianResolveHours}h</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">SLA breaches</div>
<div className="mt-1 text-lg font-semibold">{simulated.slaBreaches}</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Citizen satisfaction</div>
<div className="mt-1 text-lg font-semibold">{simulated.citizenSatisfaction} / 5</div>
</div>
</div>
<div className="rounded-2xl border bg-muted/20 p-4 text-xs text-muted-foreground">
This stays credible if you keep it conservative and show confidence bands once you have data.
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* ECONOMY & SUPPLY */}
<TabsContent value="economy" className="mt-4">
<div className="grid grid-cols-1 gap-3 lg:grid-cols-12">
<Card className="lg:col-span-7 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Demand–Supply Gap (by district × category)</CardTitle>
<CardDescription>
Your wedge: connect city needs to local supplier capacity — and prove “local-first” works.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{demandSupply
.filter((r) => (district === "All districts" ? true : r.district === district))
.filter((r) => (category === "All categories" ? true : r.category === category))
.map((r, idx) => (
<div key={idx} className="rounded-2xl border bg-background p-3">
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
<div>
<div className="flex flex-wrap items-center gap-2">
<div className="text-sm font-semibold">{r.district}</div>
<Badge variant="outline" className="rounded-full">{r.category}</Badge>
<Badge className="rounded-full inline-flex items-center gap-1">
<TrendingUp className="h-3.5 w-3.5" /> Gap {r.gap > 0 ? `+${r.gap}` : r.gap}
</Badge>
</div>
<div className="mt-1 text-xs text-muted-foreground">{r.note}</div>
</div>
<div className="w-full md:w-[220px]">
<div className="grid grid-cols-2 gap-2">
<div className="rounded-xl border bg-muted/20 px-2 py-2">
<div className="text-[10px] uppercase text-muted-foreground">Demand</div>
<div className="text-sm font-semibold">{r.demandScore}</div>
</div>
<div className="rounded-xl border bg-muted/20 px-2 py-2">
<div className="text-[10px] uppercase text-muted-foreground">Supply</div>
<div className="text-sm font-semibold">{r.supplyScore}</div>
</div>
</div>
</div>
</div>
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-3">
<div className="rounded-2xl border bg-background p-3">
<div className="flex items-center gap-2 text-sm font-medium">
<Truck className="h-4 w-4" /> Operational implication
</div>
<div className="mt-1 text-xs text-muted-foreground">
High gap → longer ETAs and SLA breaches unless you re-balance coverage.
</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="flex items-center gap-2 text-sm font-medium">
<Building2 className="h-4 w-4" /> Economic implication
</div>
<div className="mt-1 text-xs text-muted-foreground">
Gap is a target list for SME support / attracting providers.
</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="flex items-center gap-2 text-sm font-medium">
<Wrench className="h-4 w-4" /> Suggested action
</div>
<div className="mt-1 text-xs text-muted-foreground">
Broadcast micro-jobs + verify local suppliers + track completion quality.
</div>
</div>
</div>
</div>
))}
<div className="rounded-2xl border bg-muted/20 p-4 text-xs text-muted-foreground">
If this becomes real: your hardest part is *vendor registry accuracy* (licenses, categories, coverage, availability).
Don’t fake it — design the workflow to improve data quality over time.
</div>
</div>
</CardContent>
</Card>
<Card className="lg:col-span-5 shadow-sm">
<CardHeader>
<CardTitle className="text-base">Business ecosystem signals</CardTitle>
<CardDescription>Politically useful, but must be defensible</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-2">
<div className="rounded-2xl border bg-background p-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium inline-flex items-center gap-2">
<Building2 className="h-4 w-4" /> New openings (30d)
</div>
<div className="text-lg font-semibold">+28</div>
</div>
<div className="mt-1 text-xs text-muted-foreground">
Most: food service, repairs, local retail
</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium inline-flex items-center gap-2">
<Lightbulb className="h-4 w-4" /> Permit pipeline
</div>
<div className="text-lg font-semibold">312</div>
</div>
<div className="mt-1 text-xs text-muted-foreground">
Avg approval: 9.6 days • 18 blocked
</div>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="flex items-center justify-between">
<div className="text-sm font-medium inline-flex items-center gap-2">
<TrendingUp className="h-4 w-4" /> Local-first share
</div>
<div className="text-lg font-semibold">62%</div>
</div>
<div className="mt-1 text-xs text-muted-foreground">
Share of routed work completed by local providers
</div>
</div>
</div>
<div className="rounded-2xl border bg-muted/20 p-4">
<div className="text-sm font-medium">What makes this credible</div>
<ul className="mt-2 list-disc pl-5 text-xs text-muted-foreground space-y-1">
<li>Show your sources (permits, registry, verified vendors)</li>
<li>Separate “estimated” from “measured”</li>
<li>Keep numbers conservative until you have time-series evidence</li>
</ul>
</div>
<div className="rounded-2xl border bg-background p-3">
<div className="text-xs text-muted-foreground">Next best feature for Turash</div>
<div className="mt-1 text-sm">
“Broadcast micro-jobs” + vendor verification loop → grows supply data as a side effect.
</div>
<div className="mt-3 flex gap-2">
<Button size="sm" className="rounded-2xl">Open vendor registry</Button>
<Button size="sm" variant="secondary" className="rounded-2xl">View gaps</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
{/* Footer note */}
<div className="mt-6 text-xs text-muted-foreground">
Prototype intent: demonstrate the *feel* and “why it wins.” Replace mock data with your APIs, then keep the surface area small.
</div>
</div>
</div>
); } Which information do you think city management should be seing and what would be useful and appealing for turash project Thought for 8s
City management doesn’t want “more data”. They want situational awareness + accountability + budget justification in one screen. If Turash can give them that (without a 12-month IT project), it’s appealing.
What they should be seeing (the “executive cockpit”)
These are the few things mayors / city managers actually act on:
City pulse (today / this week)
of citizen issues opened/closed (311-style), backlog, SLA breaches
Top 5 problem categories trending (waste, roads, lighting, noise, parking)
Hotspots on a map (heatmap + “what changed vs last week”)
Service performance (accountability)
Median time-to-resolve by department and category
“Aging” of open issues (0–2d, 3–7d, 8–30d, 30+d)
Repeat incidents (same location/category reappearing = systemic failure)
Budget impact (the language of decisions)
Cost-to-serve per category (even rough ranges)
Overtime spend, contractor spend, unit costs (€/pothole, €/lamp repair)
“If we fund X, SLA improves by Y” (simple scenario sliders)
Economic activity (politically valuable)
New business openings/closures by district
Permit pipeline (submitted/approved/blocked + time in each stage)
Local business engagement: demand vs supply gaps (more below)
Risk & compliance (things that explode later)
Safety hazards unresolved (sidewalk/lighting/traffic)
Critical infrastructure incidents (water leaks, power issues)
Compliance queue (inspections, deadlines, renewals)
What’s uniquely useful for Turash (your edge)
Turash sounds like a city CRM + local business graph. Your unfair advantage is connecting problems → local suppliers → resolution.
City management will care about:
A) “Demand–Supply Gap” map (killer feature)
Show where citizens/municipal needs exist but local supply is missing or overloaded:
Example: lots of complaints about waste pickup in District A + few active waste contractors there
Or: high renovation permits + not enough licensed electricians
This is gold for:
Economic development (attract suppliers)
Procurement strategy (reduce response time)
Policy (support training programs in shortage trades)
B) Procurement-lite routing (without calling it procurement)
Cities hate “new procurement systems”. But they love:
“Suggested local vendors for this job category (licensed, available, near)”
“Broadcast small jobs to verified local providers”
“Track acceptance, completion, and citizen satisfaction”
That’s a real operational wedge.
C) Business ecosystem health (simple, visual, political)
“Top 10 categories growing / shrinking”
“Vacancy signals” (if you can proxy: listings, footfall, complaints, closures)
“Local spend kept local” estimate (even directional)
The 12 widgets that sell the product in 5 minutes
If you want a demo dashboard that lands, build these first:
Open issues today / closed today
SLA breach counter + top violating departments
Backlog aging histogram
Heatmap of incidents with category filters
Repeat-problem locations list (top 20)
Median resolution time trend (4-week)
Permit funnel + average approval time
Business openings/closures by district
Demand–supply gap map by category
“Suggested providers” for a selected issue (with distance/licensing)
Citizen satisfaction score after closure (1–5)
Cost estimate: “this month’s service workload” vs last month
Hard truth: cities won’t integrate 20 systems for your MVP
Assumption to kill early: “we’ll ingest all city data”. You won’t—at least not at first.
Win with a thin layer:
Start with one workflow (e.g., citizen issues + routing to local contractors)
Use manual import / CSV / email parsing / simple API initially
Prove: backlog down, resolution faster, local providers engaged
Once they see impact, integration budgets appear.
What will be “appealing” internally (how they justify buying)
Frame your outputs as:
SLA improvement (measurable)
Lower cost-to-serve (even directional)
Economic uplift (support local SMEs, reduce outside contractors)
Transparency (public dashboard option, if they’re brave)
If you tell me who the buyer is (mayor’s office, economic development, operations, or a “smart city” unit) and what Turash currently has (business directory only? CRM? issue tracking?), I’ll pin this down into a tight MVP scope and a demo flow that closes.
Can you design example in web canvas to get the feel Thought for 12s
Done — I sketched a Turash “City Management Cockpit” React prototype in canvas: filters (district/category/search), KPI row, and 3 tabs:
Pulse: hotspot list + “map” placeholder + click-to-open routing panel with suggested providers
Service performance: resolution-time trend + backlog aging + a conservative scenario slider (budget → SLA impact)
Economy & supply: Demand–Supply Gap view + “business signals” card (defensible, not fluffy)
Tell me which persona you want to optimize for first (Mayor / Ops director / Economic development / IT) and I’ll reshape the layout and the “default story” of the dashboard accordingly.
ChatGPT can make mistakes. Check important info. See Cookie Preferences.