| import { useEffect, useState } from "react" |
| import { |
| Area, |
| AreaChart, |
| Bar, |
| BarChart, |
| CartesianGrid, |
| Cell, |
| Pie, |
| PieChart, |
| ResponsiveContainer, |
| Tooltip, |
| XAxis, |
| YAxis, |
| } from "recharts" |
| import MetricCard from "../components/MetricCard" |
| import { fetchAnalyticsSummary } from "../lib/api" |
| import { formatShortTime, titleCase } from "../lib/formatters" |
|
|
| const CHART_COLORS = ["#38bdf8", "#f59e0b", "#f43f5e", "#22c55e", "#94a3b8", "#a855f7"] |
|
|
| export default function Analytics() { |
| const [analytics, setAnalytics] = useState(null) |
| const [loading, setLoading] = useState(true) |
| const [error, setError] = useState("") |
|
|
| useEffect(() => { |
| let active = true |
|
|
| fetchAnalyticsSummary(120) |
| .then((payload) => { |
| if (active) { |
| setAnalytics(payload) |
| } |
| }) |
| .catch((requestError) => { |
| if (active) { |
| setError(requestError.message) |
| } |
| }) |
| .finally(() => { |
| if (active) { |
| setLoading(false) |
| } |
| }) |
|
|
| return () => { |
| active = false |
| } |
| }, []) |
|
|
| const decisionData = analytics |
| ? Object.entries(analytics.decision_breakdown).map(([name, value]) => ({ name, value })) |
| : [] |
| const severityData = analytics |
| ? Object.entries(analytics.severity_totals).map(([name, value]) => ({ name, value })) |
| : [] |
| const sourceData = analytics |
| ? Object.entries(analytics.source_breakdown).map(([name, value]) => ({ name: titleCase(name), value })) |
| : [] |
| const defectData = analytics |
| ? Object.entries(analytics.defect_breakdown) |
| .map(([name, value]) => ({ name: titleCase(name), value })) |
| .sort((left, right) => right.value - left.value) |
| .slice(0, 6) |
| : [] |
| const timeline = analytics |
| ? analytics.timeline.map((entry) => ({ |
| ...entry, |
| label: formatShortTime(entry.timestamp), |
| })) |
| : [] |
|
|
| return ( |
| <div className="space-y-6 pb-10"> |
| <section className="surface-card p-6 md:p-8"> |
| <div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]"> |
| <div> |
| <p className="eyebrow">Analytics Page</p> |
| <h2 className="hero-title mt-3">Track quality trends, decision mix, and defect patterns across saved inspections.</h2> |
| <p className="body-copy mt-5 max-w-3xl"> |
| Use this page to spot recurring defects, monitor severity changes, and review how inspection results move over time. |
| </p> |
| </div> |
| |
| <div className="grid gap-4 sm:grid-cols-3"> |
| <MetricCard label="Reports" value={analytics?.report_count ?? "--"} detail="Trend window size." tone="steel" /> |
| <MetricCard label="Avg Defects" value={analytics?.average_total_defects ?? "--"} detail="Per inspection." tone="accent" /> |
| <MetricCard label="Sources" value={sourceData.length || "--"} detail="Capture modes in the dataset." tone="warning" /> |
| </div> |
| </div> |
| </section> |
| |
| {loading ? ( |
| <section className="surface-card p-6 text-sm text-slate-400">Loading analytics...</section> |
| ) : error ? ( |
| <section className="surface-card p-6 text-sm text-rose-100">{error}</section> |
| ) : ( |
| <> |
| <section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]"> |
| <div className="surface-card p-6"> |
| <p className="eyebrow">Trend Over Time</p> |
| <h3 className="section-title mt-2">Defect volume per inspection</h3> |
| <div className="mt-6 h-[340px]"> |
| <ResponsiveContainer width="100%" height="100%"> |
| <AreaChart data={timeline}> |
| <defs> |
| <linearGradient id="totalGradient" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="0%" stopColor="#38bdf8" stopOpacity={0.45} /> |
| <stop offset="100%" stopColor="#38bdf8" stopOpacity={0.02} /> |
| </linearGradient> |
| </defs> |
| <CartesianGrid stroke="rgba(148, 163, 184, 0.12)" vertical={false} /> |
| <XAxis dataKey="label" tick={{ fill: "#94a3b8", fontSize: 12 }} axisLine={false} tickLine={false} /> |
| <YAxis tick={{ fill: "#94a3b8", fontSize: 12 }} axisLine={false} tickLine={false} /> |
| <Tooltip |
| contentStyle={{ |
| background: "rgba(15, 23, 42, 0.96)", |
| border: "1px solid rgba(148, 163, 184, 0.18)", |
| borderRadius: "18px", |
| color: "#e2e8f0", |
| }} |
| /> |
| <Area type="monotone" dataKey="total_defects" stroke="#38bdf8" strokeWidth={3} fill="url(#totalGradient)" /> |
| </AreaChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| |
| <div className="surface-card p-6"> |
| <p className="eyebrow">Decision Mix</p> |
| <h3 className="section-title mt-2">PASS, REVIEW, and FAIL distribution</h3> |
| <div className="mt-6 h-[340px]"> |
| <ResponsiveContainer width="100%" height="100%"> |
| <PieChart> |
| <Tooltip |
| contentStyle={{ |
| background: "rgba(15, 23, 42, 0.96)", |
| border: "1px solid rgba(148, 163, 184, 0.18)", |
| borderRadius: "18px", |
| color: "#e2e8f0", |
| }} |
| /> |
| <Pie data={decisionData} dataKey="value" nameKey="name" innerRadius={70} outerRadius={110} paddingAngle={4}> |
| {decisionData.map((entry, index) => ( |
| <Cell key={entry.name} fill={CHART_COLORS[index % CHART_COLORS.length]} /> |
| ))} |
| </Pie> |
| </PieChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| </section> |
| |
| <section className="grid gap-6 xl:grid-cols-[0.9fr_1.1fr]"> |
| <div className="surface-card p-6"> |
| <p className="eyebrow">Severity Totals</p> |
| <h3 className="section-title mt-2">Minor, moderate, and critical volume</h3> |
| <div className="mt-6 h-[340px]"> |
| <ResponsiveContainer width="100%" height="100%"> |
| <BarChart data={severityData}> |
| <CartesianGrid stroke="rgba(148, 163, 184, 0.12)" vertical={false} /> |
| <XAxis dataKey="name" tick={{ fill: "#94a3b8", fontSize: 12 }} axisLine={false} tickLine={false} /> |
| <YAxis tick={{ fill: "#94a3b8", fontSize: 12 }} axisLine={false} tickLine={false} /> |
| <Tooltip |
| contentStyle={{ |
| background: "rgba(15, 23, 42, 0.96)", |
| border: "1px solid rgba(148, 163, 184, 0.18)", |
| borderRadius: "18px", |
| color: "#e2e8f0", |
| }} |
| /> |
| <Bar dataKey="value" radius={[14, 14, 0, 0]}> |
| {severityData.map((entry, index) => ( |
| <Cell key={entry.name} fill={CHART_COLORS[index % CHART_COLORS.length]} /> |
| ))} |
| </Bar> |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| |
| <div className="surface-card p-6"> |
| <p className="eyebrow">Defect Types</p> |
| <h3 className="section-title mt-2">Most frequent defect categories</h3> |
| <div className="mt-6 h-[340px]"> |
| <ResponsiveContainer width="100%" height="100%"> |
| <BarChart layout="vertical" data={defectData}> |
| <CartesianGrid stroke="rgba(148, 163, 184, 0.12)" horizontal={false} /> |
| <XAxis type="number" tick={{ fill: "#94a3b8", fontSize: 12 }} axisLine={false} tickLine={false} /> |
| <YAxis type="category" dataKey="name" tick={{ fill: "#cbd5e1", fontSize: 12 }} axisLine={false} tickLine={false} width={120} /> |
| <Tooltip |
| contentStyle={{ |
| background: "rgba(15, 23, 42, 0.96)", |
| border: "1px solid rgba(148, 163, 184, 0.18)", |
| borderRadius: "18px", |
| color: "#e2e8f0", |
| }} |
| /> |
| <Bar dataKey="value" radius={[0, 14, 14, 0]}> |
| {defectData.map((entry, index) => ( |
| <Cell key={entry.name} fill={CHART_COLORS[index % CHART_COLORS.length]} /> |
| ))} |
| </Bar> |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| </section> |
| |
| <section className="grid gap-6 xl:grid-cols-2"> |
| <div className="surface-card p-6"> |
| <p className="eyebrow">Capture Sources</p> |
| <h3 className="section-title mt-2">Which input modes are being used</h3> |
| <div className="mt-5 space-y-3"> |
| {sourceData.length ? ( |
| sourceData.map((entry, index) => ( |
| <div key={entry.name} className="flex items-center justify-between rounded-3xl border border-white/10 bg-white/5 px-4 py-4"> |
| <div className="flex items-center gap-3"> |
| <span |
| className="inline-flex h-3.5 w-3.5 rounded-full" |
| style={{ backgroundColor: CHART_COLORS[index % CHART_COLORS.length] }} |
| /> |
| <p className="text-sm text-slate-200">{entry.name}</p> |
| </div> |
| <p className="text-sm font-medium text-slate-100">{entry.value}</p> |
| </div> |
| )) |
| ) : ( |
| <div className="rounded-3xl border border-dashed border-white/15 bg-white/5 p-5 text-sm text-slate-400"> |
| Source analytics will appear once reports are available. |
| </div> |
| )} |
| </div> |
| </div> |
| |
| <div className="surface-card p-6"> |
| <p className="eyebrow">Monitoring Notes</p> |
| <h3 className="section-title mt-2">Practical guidance for better inspection quality</h3> |
| <div className="mt-5 grid gap-4 text-sm leading-7 text-slate-300"> |
| <div className="rounded-3xl border border-white/10 bg-white/5 p-4"> |
| Keep the steel surface centered with a top-down view for more stable live monitoring. |
| </div> |
| <div className="rounded-3xl border border-white/10 bg-white/5 p-4"> |
| Use uploaded images for detailed review and save live logging for confirmed production checks. |
| </div> |
| <div className="rounded-3xl border border-white/10 bg-white/5 p-4"> |
| Enable deep AI analysis only when you need slower recommendation text beyond the core inspection result. |
| </div> |
| </div> |
| </div> |
| </section> |
| </> |
| )} |
| </div> |
| ) |
| } |
|
|