diff --git a/src/App.tsx b/src/App.tsx index 138ec713..2fd417e7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,6 +39,11 @@ import { Settings } from "./pages/admin/Settings"; import IntegrationsMarketplace from "./pages/admin/IntegrationsMarketplace"; import Shipments from "./pages/admin/Shipments"; import StepsTemplatesView from "./pages/admin/StepsTemplatesView"; +import AnalyticsDashboard from "./pages/admin/Analytics"; +import OEEAnalytics from "./pages/admin/analytics/OEEAnalytics"; +import ReliabilityAnalytics from "./pages/admin/analytics/ReliabilityAnalytics"; +import QRMAnalytics from "./pages/admin/analytics/QRMAnalytics"; +import QRMDashboard from "./pages/admin/analytics/QRMDashboard"; import ApiDocs from "./pages/ApiDocs"; import Pricing from "./pages/Pricing"; import { MyPlan } from "./pages/MyPlan"; @@ -449,6 +454,62 @@ function AppRoutes() { } /> + {/* Analytics Routes */} + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + + + + + + + } + /> + { + // Mock Data + const oeeData = [ + { name: "Availability", value: 85, fill: "hsl(var(--brand-primary))" }, + { name: "Performance", value: 92, fill: "hsl(var(--brand-accent))" }, + { name: "Quality", value: 98, fill: "hsl(var(--color-success))" }, + ]; + + const machineStateData = [ + { name: "Running", value: 65, color: "hsl(var(--color-success))" }, + { name: "Idle", value: 20, color: "hsl(var(--color-warning))" }, + { name: "Down", value: 10, color: "hsl(var(--color-error))" }, + { name: "Maintenance", value: 5, color: "hsl(var(--neutral-400))" }, + ]; + + const trendData = [ + { name: "Mon", oee: 82 }, + { name: "Tue", oee: 84 }, + { name: "Wed", oee: 88 }, + { name: "Thu", oee: 85 }, + { name: "Fri", oee: 90 }, + { name: "Sat", oee: 87 }, + { name: "Sun", oee: 89 }, + ]; + + return ( +
+ {/* OEE Breakdown */} + + + OEE Breakdown + + +
+ + + + + + + + + +
+
+
+ + {/* Machine States */} + + + Machine States + + +
+ + + + {machineStateData.map((entry, index) => ( + + ))} + + + + + +
+
+
+ + {/* OEE Trend */} + + + OEE Trend (Last 7 Days) + + +
+ + + + + + + + + +
+
+
+
+ ); +}; + +export { OEECharts }; diff --git a/src/components/analytics/QRMCharts.tsx b/src/components/analytics/QRMCharts.tsx new file mode 100644 index 00000000..48d80c99 --- /dev/null +++ b/src/components/analytics/QRMCharts.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + ComposedChart, + Line, +} from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const QRMCharts = () => { + // Mock Data + const leadTimeData = [ + { product: "Prod A", mct: 12, touchTime: 4 }, + { product: "Prod B", mct: 18, touchTime: 6 }, + { product: "Prod C", mct: 8, touchTime: 3 }, + { product: "Prod D", mct: 24, touchTime: 8 }, + ]; + + const authBacklogData = [ + { day: "Mon", pending: 5, approved: 12 }, + { day: "Tue", pending: 8, approved: 10 }, + { day: "Wed", pending: 4, approved: 15 }, + { day: "Thu", pending: 6, approved: 14 }, + { day: "Fri", pending: 3, approved: 18 }, + ]; + + return ( +
+ {/* MCT Analysis */} + + + MCT vs Touch Time (Hours) + + +
+ + + + + + + + + + + +
+
+
+ + {/* Authorization Backlog */} + + + Authorization Backlog Trend + + +
+ + + + + + + + + + + +
+
+
+
+ ); +}; + +export { QRMCharts }; diff --git a/src/components/analytics/QRMDashboardCharts.tsx b/src/components/analytics/QRMDashboardCharts.tsx new file mode 100644 index 00000000..8c486024 --- /dev/null +++ b/src/components/analytics/QRMDashboardCharts.tsx @@ -0,0 +1,385 @@ +import React from "react"; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ReferenceLine, + BarChart, + Bar, + Cell, + ComposedChart, + Line, + PieChart, + Pie, +} from "recharts"; +import { useTranslation } from "react-i18next"; +import { QRMDashboardMetrics } from "@/hooks/useQRMDashboardMetrics"; + +// --- Colors from Design System --- +const COLORS = { + primary: "hsl(var(--brand-primary))", + success: "hsl(var(--color-success))", + warning: "hsl(var(--color-warning))", + error: "hsl(var(--color-error))", + info: "hsl(var(--color-info))", + muted: "hsl(var(--muted-foreground))", + grid: "rgba(255,255,255,0.1)", +}; + +// --- MCT Chart --- +export const MCTChart = ({ data }: { data: QRMDashboardMetrics["mct"] }) => { + const { t } = useTranslation(); + + return ( +
+
+ {data.current.toFixed(1)} + {t("qrm.mct.days")} +
+
+ + + + + + + + + + + + + + + + +
+
+ ); +}; + +// --- OTP Gauge --- +export const OTPGauge = ({ data }: { data: QRMDashboardMetrics["otp"] }) => { + const { t } = useTranslation(); + + // Calculate color based on value + const getColor = (value: number) => { + if (value >= 95) return COLORS.success; + if (value >= 85) return COLORS.warning; + return COLORS.error; + }; + + const color = getColor(data.current); + + // Data for the semi-circle gauge + const gaugeData = [ + { name: "value", value: data.current }, + { name: "remainder", value: 100 - data.current }, + ]; + + return ( +
+
+ + + + + + + + +
+ {data.current.toFixed(1)}% + {t("qrm.otp.onTime")} +
+
+ {/* Mini Trend Line */} +
+ + + + + +
+
+ ); +}; + +// --- Queue Time Chart --- +export const QueueTimeChart = ({ data }: { data: QRMDashboardMetrics["queueTime"] }) => { + const { t } = useTranslation(); + + return ( +
+ + + + + + + + {data.byCell.map((entry, index) => ( + + ))} + + + +
+ ); +}; + +// --- Cycle Time Chart (Range Bar simulated with ComposedChart) --- +export const CycleTimeChart = ({ data }: { data: QRMDashboardMetrics["cycleTime"] }) => { + const { t } = useTranslation(); + + // Transform data for range visualization + // We'll use a bar for the range (min to max) and a line or scatter for median + // Actually, Recharts doesn't support box plots natively well. + // We can use a stacked bar approach: [transparent (min), bar (max-min)] + // But to show median, we might need a custom shape or composed chart. + + // Simplified approach: Bar chart showing Median, with Error Bars for Min/Max? + // Or just a simple bar chart of Median for now to keep it clean, maybe with a "range" tooltip. + + return ( +
+ + + + + + { + if (name === "median") return [`${value} min`, "Median"]; + return [`${value} min`, name]; + }} + /> + + + +
+ ); +}; + +// --- WIP Age Chart --- +export const WIPAgeChart = ({ data }: { data: QRMDashboardMetrics["wipAge"] }) => { + const { t } = useTranslation(); + + // Gradient colors for age buckets + const AGE_COLORS = [ + "hsl(var(--color-success))", // 0-2 days + "hsl(var(--color-info))", // 3-5 days + "hsl(var(--color-warning))", // 6-10 days + "hsl(var(--color-error))", // >10 days + ]; + + return ( +
+
+ {t("qrm.wipAge.jobs")} + {data.totalWip} +
+
+ + + + + + + + {data.distribution.map((entry, index) => ( + + ))} + + + +
+
+ ); +}; + +// --- Issue Rate Chart --- +export const IssueRateChart = ({ data }: { data: QRMDashboardMetrics["issueRate"] }) => { + const { t } = useTranslation(); + + return ( +
+ + + + + + + + + +
+ ); +}; + +// --- Reliability Heatmap --- +export const ReliabilityHeatmap = ({ data }: { data: QRMDashboardMetrics["reliability"] }) => { + const { t } = useTranslation(); + + // Helper to get color based on reliability score + const getCellColor = (value: number) => { + if (value >= 95) return "hsl(var(--color-success))"; + if (value >= 90) return "hsl(var(--color-info))"; + if (value >= 80) return "hsl(var(--color-warning))"; + return "hsl(var(--color-error))"; + }; + + return ( +
+ + + + + {data.periodLabels.map((label, i) => ( + + ))} + + + + {data.heatmap.map((row, i) => ( + + + {row.values.map((val, j) => ( + + ))} + + ))} + +
{t("qrm.reliability.cell")}{label}
{row.cellName} +
+ {val} +
+
+
+ ); +}; + +// --- Throughput Chart --- +export const ThroughputChart = ({ data }: { data: QRMDashboardMetrics["throughput"] }) => { + const { t } = useTranslation(); + + return ( +
+
+ {data.byCell.map((cell, index) => ( +
+
+ {cell.cellName} +
+
+ + ({ i, val }))}> + + + + + + + + [`${value} units`, "Throughput"]} + /> + + +
+
+ {cell.current} u/d +
+
+ ))} +
+
+ ); +}; diff --git a/src/components/analytics/ReliabilityCharts.tsx b/src/components/analytics/ReliabilityCharts.tsx new file mode 100644 index 00000000..6796f65d --- /dev/null +++ b/src/components/analytics/ReliabilityCharts.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + AreaChart, + Area, +} from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const ReliabilityCharts = () => { + // Mock Data + const reliabilityData = [ + { date: "Week 1", onTime: 88, late: 12 }, + { date: "Week 2", onTime: 92, late: 8 }, + { date: "Week 3", onTime: 85, late: 15 }, + { date: "Week 4", onTime: 95, late: 5 }, + { date: "Week 5", onTime: 90, late: 10 }, + ]; + + const startDelayData = [ + { date: "Mon", delay: 15 }, // minutes + { date: "Tue", delay: 5 }, + { date: "Wed", delay: 25 }, + { date: "Thu", delay: 10 }, + { date: "Fri", delay: 8 }, + ]; + + return ( +
+ {/* On-Time Start Performance */} + + + On-Time Start Performance + + +
+ + + + + + + + + + + + + + + + +
+
+
+ + {/* Average Start Delay */} + + + Average Start Delay (Minutes) + + +
+ + + + + + + + + +
+
+
+
+ ); +}; + +export { ReliabilityCharts }; diff --git a/src/hooks/useQRMDashboardMetrics.ts b/src/hooks/useQRMDashboardMetrics.ts new file mode 100644 index 00000000..7d70eb13 --- /dev/null +++ b/src/hooks/useQRMDashboardMetrics.ts @@ -0,0 +1,349 @@ +import { useQuery } from "@tanstack/react-query"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/contexts/AuthContext"; +import { subDays, startOfDay, endOfDay, format, startOfWeek, endOfWeek, subWeeks, eachDayOfInterval } from "date-fns"; + +export interface QRMDashboardMetrics { + mct: { + current: number; + trend: { date: string; value: number }[]; + target: number; + }; + otp: { + current: number; + trend: { date: string; value: number }[]; + }; + queueTime: { + byCell: { cellName: string; avgQueueTime: number }[]; + }; + cycleTime: { + byOperation: { operationType: string; min: number; q1: number; median: number; q3: number; max: number }[]; + }; + wipAge: { + distribution: { bucket: string; count: number; label: string }[]; + totalWip: number; + }; + issueRate: { + byCategory: { category: string; rate: number; count: number }[]; + totalIssues: number; + }; + throughput: { + byCell: { cellName: string; current: number; trend: number[] }[]; + }; + reliability: { + heatmap: { cellName: string; values: number[] }[]; + periodLabels: string[]; + }; +} + +export function useQRMDashboardMetrics(dateRange: number = 30) { + const { profile } = useAuth(); + + return useQuery({ + queryKey: ["qrm-dashboard-metrics", profile?.tenant_id, dateRange], + queryFn: async (): Promise => { + if (!profile?.tenant_id) { + throw new Error("No tenant ID"); + } + + const startDate = startOfDay(subDays(new Date(), dateRange)).toISOString(); + const endDate = endOfDay(new Date()).toISOString(); + + // 1. Fetch Jobs (MCT, OTP, WIP) + const { data: jobs, error: jobsError } = await supabase + .from("jobs") + .select("created_at, updated_at, due_date, status") + .eq("tenant_id", profile.tenant_id) + .or(`updated_at.gte.${startDate},status.eq.in_progress,status.eq.pending`); + + if (jobsError) throw jobsError; + + // 2. Fetch Operations (Queue, Cycle, Throughput, Reliability) + const { data: operations, error: opsError } = await supabase + .from("operations") + .select(` + id, + actual_time, + wait_time, + completed_at, + status, + cell_id, + cells(name), + operation_name, + planned_end + `) + .eq("tenant_id", profile.tenant_id) + .gte("updated_at", startDate); + + if (opsError) throw opsError; + + // 3. Fetch Issues (Issue Rate) + const { data: issues, error: issuesError } = await supabase + .from("issues") + .select("ncr_category, created_at") + .eq("tenant_id", profile.tenant_id) + .gte("created_at", startDate); + + if (issuesError) throw issuesError; + + // --- Calculations --- + + // Generate date range for trends + const now = new Date(); + const startDateTrend = subDays(now, dateRange); + const dateInterval = eachDayOfInterval({ start: startDateTrend, end: now }); + + // Calculate sampling interval to get ~10-12 data points for trends + const trendSampleInterval = Math.max(1, Math.floor(dateRange / 10)); + + // Calculate number of periods for reliability (weeks for 30+ days, days for shorter) + const numPeriods = Math.min(8, Math.max(4, Math.ceil(dateRange / 7))); + + // MCT & OTP with trends + const completedJobs = jobs?.filter(j => j.status === 'completed' && j.updated_at) || []; + const mctValues = completedJobs.map(j => { + const start = new Date(j.created_at!).getTime(); + const end = new Date(j.updated_at!).getTime(); + return (end - start) / (1000 * 60 * 60 * 24); // Days + }); + const avgMct = mctValues.length ? mctValues.reduce((a, b) => a + b, 0) / mctValues.length : 0; + + const onTimeJobs = completedJobs.filter(j => j.due_date && new Date(j.updated_at!) <= new Date(j.due_date)); + const otp = completedJobs.length ? (onTimeJobs.length / completedJobs.length) * 100 : 100; + + // MCT trend - group by day + const mctByDay = new Map(); + completedJobs.forEach(j => { + const day = format(new Date(j.updated_at!), 'yyyy-MM-dd'); + const mctDays = (new Date(j.updated_at!).getTime() - new Date(j.created_at!).getTime()) / (1000 * 60 * 60 * 24); + if (!mctByDay.has(day)) mctByDay.set(day, []); + mctByDay.get(day)!.push(mctDays); + }); + + const mctTrend = dateInterval.filter((_, i) => i % trendSampleInterval === 0).map(date => { + const day = format(date, 'yyyy-MM-dd'); + const dayMcts = mctByDay.get(day) || []; + const avgDayMct = dayMcts.length ? dayMcts.reduce((a, b) => a + b, 0) / dayMcts.length : avgMct; + return { date: format(date, 'MMM d'), value: Number(avgDayMct.toFixed(1)) }; + }); + + // OTP trend - calculate daily OTP + const otpByDay = new Map(); + completedJobs.forEach(j => { + const day = format(new Date(j.updated_at!), 'yyyy-MM-dd'); + if (!otpByDay.has(day)) otpByDay.set(day, { onTime: 0, total: 0 }); + const dayData = otpByDay.get(day)!; + dayData.total++; + if (j.due_date && new Date(j.updated_at!) <= new Date(j.due_date)) { + dayData.onTime++; + } + }); + + const otpTrend = dateInterval.filter((_, i) => i % trendSampleInterval === 0).map(date => { + const day = format(date, 'yyyy-MM-dd'); + const dayData = otpByDay.get(day); + const dayOtp = dayData && dayData.total > 0 ? (dayData.onTime / dayData.total) * 100 : otp; + return { date: format(date, 'MMM d'), value: Number(dayOtp.toFixed(1)) }; + }); + + // Queue Time by Cell + const queueByCellMap = new Map(); + operations?.forEach(op => { + if (op.wait_time && op.cells?.name) { + const current = queueByCellMap.get(op.cells.name) || { total: 0, count: 0 }; + queueByCellMap.set(op.cells.name, { total: current.total + (op.wait_time / 60), count: current.count + 1 }); // wait_time is likely mins, convert to hours? Assuming mins based on schema context usually. Let's assume wait_time is minutes. + } + }); + const queueTimeByCell = Array.from(queueByCellMap.entries()).map(([name, data]) => ({ + cellName: name, + avgQueueTime: data.count ? data.total / data.count : 0 + })).sort((a, b) => b.avgQueueTime - a.avgQueueTime).slice(0, 8); + + // Cycle Time by Operation Type (using operation_name as proxy for type if no explicit type) + // Group by operation name for now + const cycleMap = new Map(); + operations?.forEach(op => { + if (op.actual_time && op.operation_name) { + const current = cycleMap.get(op.operation_name) || []; + current.push(op.actual_time); // actual_time in mins + cycleMap.set(op.operation_name, current); + } + }); + const cycleTimeByOperation = Array.from(cycleMap.entries()).map(([name, times]) => { + times.sort((a, b) => a - b); + const q1 = times[Math.floor(times.length * 0.25)]; + const median = times[Math.floor(times.length * 0.5)]; + const q3 = times[Math.floor(times.length * 0.75)]; + return { + operationType: name, + min: times[0], + q1, + median, + q3, + max: times[times.length - 1] + }; + }).slice(0, 8); + + // WIP Age + const activeJobs = jobs?.filter(j => j.status === 'in_progress' || j.status === 'pending') || []; + const wipBuckets = { "0-2d": 0, "3-5d": 0, "6-10d": 0, ">10d": 0 }; + activeJobs.forEach(j => { + const age = (new Date().getTime() - new Date(j.created_at).getTime()) / (1000 * 60 * 60 * 24); + if (age <= 2) wipBuckets["0-2d"]++; + else if (age <= 5) wipBuckets["3-5d"]++; + else if (age <= 10) wipBuckets["6-10d"]++; + else wipBuckets[">10d"]++; + }); + const wipAgeDistribution = [ + { bucket: "0-2 days", count: wipBuckets["0-2d"], label: "0-2d" }, + { bucket: "3-5 days", count: wipBuckets["3-5d"], label: "3-5d" }, + { bucket: "6-10 days", count: wipBuckets["6-10d"], label: "6-10d" }, + { bucket: ">10 days", count: wipBuckets[">10d"], label: ">10d" }, + ]; + + // Issue Rate + const issueMap = new Map(); + issues?.forEach(i => { + const cat = i.ncr_category || "Uncategorized"; + issueMap.set(cat, (issueMap.get(cat) || 0) + 1); + }); + const totalOps = operations?.filter(o => o.status === 'completed').length || 1; + const issueRateByCategory = Array.from(issueMap.entries()).map(([cat, count]) => ({ + category: cat, + count, + rate: (count / totalOps) * 100 + })).sort((a, b) => b.count - a.count).slice(0, 5); + + // Throughput by cell with real daily trends + const completedOps = operations?.filter(o => o.status === 'completed' && o.completed_at) || []; + const throughputByCellDay = new Map>(); + + completedOps.forEach(op => { + if (op.cells?.name && op.completed_at) { + const cell = op.cells.name; + const day = format(new Date(op.completed_at), 'yyyy-MM-dd'); + + if (!throughputByCellDay.has(cell)) { + throughputByCellDay.set(cell, new Map()); + } + const cellMap = throughputByCellDay.get(cell)!; + cellMap.set(day, (cellMap.get(day) || 0) + 1); + } + }); + + // Generate trend days based on dateRange (use ~14 points for sparklines) + const sparklineDays = Math.min(dateRange, 14); + const trendDays = eachDayOfInterval({ start: subDays(now, sparklineDays - 1), end: now }); + + const throughputByCell = Array.from(throughputByCellDay.entries()).map(([name, dayMap]) => { + const trend = trendDays.map(date => { + const day = format(date, 'yyyy-MM-dd'); + return dayMap.get(day) || 0; + }); + const totalOps = trend.reduce((a, b) => a + b, 0); + const daysWithData = trend.filter(v => v > 0).length || 1; + return { + cellName: name, + current: Math.round(totalOps / daysWithData), + trend + }; + }).sort((a, b) => b.current - a.current); + + // Reliability Heatmap - calculate OTP per cell per period (dynamic based on dateRange) + // Generate period boundaries based on numPeriods + const periodDays = Math.floor(dateRange / numPeriods); + const periodStarts: Date[] = []; + const periodLabels: string[] = []; + + for (let i = numPeriods - 1; i >= 0; i--) { + const periodStart = subDays(now, i * periodDays + periodDays); + const periodEnd = subDays(now, i * periodDays); + periodStarts.push(periodStart); + periodLabels.push(`${format(periodStart, 'MMM d')}-${format(periodEnd, 'd')}`); + } + + // Group operations by cell and period, calculate reliability + const reliabilityByCell = new Map(); + + completedOps.forEach(op => { + if (!op.cells?.name || !op.completed_at) return; + + const cellName = op.cells.name; + const completedDate = new Date(op.completed_at); + + // Initialize cell if needed + if (!reliabilityByCell.has(cellName)) { + reliabilityByCell.set(cellName, Array.from({ length: numPeriods }, () => ({ onTime: 0, total: 0 }))); + } + + const cellData = reliabilityByCell.get(cellName)!; + + // Determine which period this operation belongs to + for (let p = 0; p < numPeriods; p++) { + const periodStart = periodStarts[p]; + const periodEnd = p < numPeriods - 1 ? periodStarts[p + 1] : now; + + if (completedDate >= periodStart && completedDate < periodEnd) { + cellData[p].total++; + // On-time if completed before or on planned_end, or if no planned_end (assume on-time) + if (!op.planned_end || completedDate <= new Date(op.planned_end)) { + cellData[p].onTime++; + } + break; + } + } + }); + + // Convert to percentage-based heatmap with dynamic values array + const reliabilityHeatmap = Array.from(reliabilityByCell.entries()) + .map(([cellName, periods]) => ({ + cellName, + values: periods.map(p => p.total > 0 ? Math.round((p.onTime / p.total) * 100) : 100) + })) + .sort((a, b) => a.cellName.localeCompare(b.cellName)) + .slice(0, 8); // Limit to 8 cells + + + // Calculate MCT target from historical 75th percentile (achievable stretch goal) + const sortedMcts = [...mctValues].sort((a, b) => a - b); + const mctTarget = sortedMcts.length > 0 + ? sortedMcts[Math.floor(sortedMcts.length * 0.25)] // 25th percentile = faster than 75% of historical jobs + : avgMct * 0.8; // Fallback: 20% better than current average + + return { + mct: { + current: avgMct, + trend: mctTrend, + target: Number(mctTarget.toFixed(1)), + }, + otp: { + current: otp, + trend: otpTrend, + }, + queueTime: { + byCell: queueTimeByCell, + }, + cycleTime: { + byOperation: cycleTimeByOperation, + }, + wipAge: { + distribution: wipAgeDistribution, + totalWip: activeJobs.length, + }, + issueRate: { + byCategory: issueRateByCategory, + totalIssues: issues?.length || 0, + }, + throughput: { + byCell: throughputByCell, + }, + reliability: { + heatmap: reliabilityHeatmap, + periodLabels, + }, + }; + }, + enabled: !!profile?.tenant_id, + staleTime: 60000, // 1 minute + }); +} diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index e05bc58d..4836f3b6 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1324,7 +1324,45 @@ "partsInCell": "{{count}} Teil(e) in Zelle", "statusCompleted": "Abgeschlossen", "statusInProgress": "In Bearbeitung", - "statusNotStarted": "Nicht gestartet" + "statusNotStarted": "Nicht gestartet", + "dashboardTitle": "QRM Produktions-Dashboard", + "dashboardSubtitle": "Echtzeit-Kennzahlen für Quick Response Manufacturing Leistung.", + "last30Days": "Letzte 30 Tage", + "refresh": "Aktualisieren", + "mct": { + "title": "MCT Trend (Manufacturing Critical-path Time)", + "days": "Tage", + "target": "Ziel" + }, + "otp": { + "title": "Lieferzuverlässigkeit (OTP)", + "onTime": "Pünktlich" + }, + "queueTime": { + "title": "Wartezeit pro Zelle", + "hours": "Stunden" + }, + "cycleTime": { + "title": "Zykluszeit pro Arbeitsgang", + "minutes": "Minuten" + }, + "wipAge": { + "title": "WIP Altersverteilung", + "jobs": "Aufträge" + }, + "issueRate": { + "title": "Problemquote nach Kategorie", + "rate": "Quote (%)" + }, + "throughput": { + "title": "Durchsatz pro Zelle", + "units": "Einheiten/Tag" + }, + "reliability": { + "title": "Zellen Zuverlässigkeit Heatmap", + "cell": "Zelle", + "week": "Woche" + } }, "terminal": { "columns": { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index cb61ea06..87a18ab7 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -814,6 +814,46 @@ "searchIssues": "Search issues...", "noIssues": "No issues found" }, + "qrm": { + "dashboardTitle": "QRM Production Dashboard", + "dashboardSubtitle": "Real-time metrics for Quick Response Manufacturing performance.", + "last30Days": "Last 30 Days", + "refresh": "Refresh", + "mct": { + "title": "MCT Trend (Manufacturing Critical-path Time)", + "days": "Days", + "target": "Target" + }, + "otp": { + "title": "Delivery Reliability (OTP)", + "onTime": "On Time" + }, + "queueTime": { + "title": "Queue Time by Cell", + "hours": "Hours" + }, + "cycleTime": { + "title": "Operation Cycle Time", + "minutes": "Minutes" + }, + "wipAge": { + "title": "WIP Age Distribution", + "jobs": "Jobs" + }, + "issueRate": { + "title": "Issue Rate by Category", + "rate": "Rate (%)" + }, + "throughput": { + "title": "Throughput by Cell", + "units": "Units/Day" + }, + "reliability": { + "title": "Cell Reliability Heatmap", + "cell": "Cell", + "week": "Week" + } + }, "stages": { "title": "Stage Configuration", "manageStages": "Manage stage configurations and workflow", @@ -1834,6 +1874,41 @@ "hoursUnit": "hours", "hoursPerDayHelp": "Maximum work hours this cell can process per day. Used by the scheduler to allocate operations." }, + "qrm": { + "dashboardTitle": "QRM Production Dashboard", + "dashboardSubtitle": "Real-time metrics for Quick Response Manufacturing performance.", + "last30Days": "Last 30 Days", + "refresh": "Refresh", + "mct": { + "title": "MCT Trend (Manufacturing Critical-path Time)", + "days": "Days", + "target": "Target" + }, + "otp": { + "title": "Delivery Reliability (OTP)", + "onTime": "On Time" + }, + "queueTime": { + "title": "Queue Time by Cell", + "hours": "Hours" + }, + "cycleTime": { + "title": "Operation Cycle Time", + "minutes": "Minutes" + }, + "wipAge": { + "title": "WIP Age Distribution", + "jobs": "Jobs" + }, + "issueRate": { + "title": "Issue Rate by Category", + "rate": "Rate (%)" + }, + "throughput": { + "title": "Throughput by Cell", + "units": "Units/Day" + } + }, "calendar": { "title": "Factory Calendar", "description": "Manage holidays, closures, and special working days" diff --git a/src/i18n/locales/nl/translation.json b/src/i18n/locales/nl/translation.json index 3385ee50..c02c1e3e 100644 --- a/src/i18n/locales/nl/translation.json +++ b/src/i18n/locales/nl/translation.json @@ -1594,7 +1594,45 @@ "partsInCell": "{{count}} onderde(e)l(en) in cel", "statusCompleted": "Voltooid", "statusInProgress": "Bezig", - "statusNotStarted": "Niet gestart" + "statusNotStarted": "Niet gestart", + "dashboardTitle": "QRM Productie Dashboard", + "dashboardSubtitle": "Real-time metrics voor Quick Response Manufacturing prestaties.", + "last30Days": "Laatste 30 dagen", + "refresh": "Vernieuwen", + "mct": { + "title": "MCT Trend (Manufacturing Critical-path Time)", + "days": "Dagen", + "target": "Doel" + }, + "otp": { + "title": "Leverbetrouwbaarheid (OTP)", + "onTime": "Op tijd" + }, + "queueTime": { + "title": "Wachttijd per cel", + "hours": "Uren" + }, + "cycleTime": { + "title": "Cyclustijd per bewerking", + "minutes": "Minuten" + }, + "wipAge": { + "title": "WIP Leeftijdsverdeling", + "jobs": "Opdrachten" + }, + "issueRate": { + "title": "Problemenpercentage per categorie", + "rate": "Percentage (%)" + }, + "throughput": { + "title": "Doorvoer per cel", + "units": "Eenheden/dag" + }, + "reliability": { + "title": "Cel Betrouwbaarheid Heatmap", + "cell": "Cel", + "week": "Week" + } }, "terminal": { "columns": { diff --git a/src/pages/admin/Analytics.tsx b/src/pages/admin/Analytics.tsx new file mode 100644 index 00000000..7897e82d --- /dev/null +++ b/src/pages/admin/Analytics.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { ROUTES } from "@/routes"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { BarChart3, Clock, Activity, ArrowRight } from "lucide-react"; + +const AnalyticsDashboard = () => { + const navigate = useNavigate(); + + const cards = [ + { + title: "OEE & State Analysis", + description: "Monitor Overall Equipment Effectiveness, availability, performance, and quality metrics.", + icon: , + route: ROUTES.ADMIN.ANALYTICS.OEE, + color: "bg-blue-500/10 border-blue-500/20", + }, + { + title: "Production Reliability", + description: "Analyze production start date reliability and schedule adherence trends.", + icon: , + route: ROUTES.ADMIN.ANALYTICS.RELIABILITY, + color: "bg-amber-500/10 border-amber-500/20", + }, + { + title: "QRM Indicators", + description: "Track Quick Response Manufacturing metrics, lead times, and authorization delays.", + icon: , + route: ROUTES.ADMIN.ANALYTICS.QRM_DASHBOARD, + color: "bg-green-500/10 border-green-500/20", + }, + ]; + + return ( +
+
+

Analytics Dashboard

+

+ Gain insights into production performance, reliability, and lead times. +

+
+ +
+ {cards.map((card) => ( +
navigate(card.route)} + > +
+ + +
+
+ {card.icon} +
+ +
+ {card.title} +
+ + + {card.description} + + +
+ ))} +
+
+ ); +}; + +export default AnalyticsDashboard; diff --git a/src/pages/admin/analytics/OEEAnalytics.tsx b/src/pages/admin/analytics/OEEAnalytics.tsx new file mode 100644 index 00000000..deeb5532 --- /dev/null +++ b/src/pages/admin/analytics/OEEAnalytics.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { OEECharts } from "@/components/analytics/OEECharts"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { ROUTES } from "@/routes"; + +const OEEAnalytics = () => { + const navigate = useNavigate(); + + return ( +
+
+ +
+

OEE & State Analysis

+

+ Detailed breakdown of Overall Equipment Effectiveness and machine states. +

+
+
+ + +
+ ); +}; + +export default OEEAnalytics; diff --git a/src/pages/admin/analytics/QRMAnalytics.tsx b/src/pages/admin/analytics/QRMAnalytics.tsx new file mode 100644 index 00000000..ba6b77ef --- /dev/null +++ b/src/pages/admin/analytics/QRMAnalytics.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { QRMCharts } from "@/components/analytics/QRMCharts"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { ROUTES } from "@/routes"; + +const QRMAnalytics = () => { + const navigate = useNavigate(); + + return ( +
+
+ +
+

QRM Indicators

+

+ Quick Response Manufacturing metrics and authorization analysis. +

+
+
+ + +
+ ); +}; + +export default QRMAnalytics; diff --git a/src/pages/admin/analytics/QRMDashboard.tsx b/src/pages/admin/analytics/QRMDashboard.tsx new file mode 100644 index 00000000..968452c2 --- /dev/null +++ b/src/pages/admin/analytics/QRMDashboard.tsx @@ -0,0 +1,190 @@ +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft, RefreshCw, Calendar } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { ROUTES } from "@/routes"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useTranslation } from "react-i18next"; +import { useQRMDashboardMetrics } from "@/hooks/useQRMDashboardMetrics"; +import { + MCTChart, + OTPGauge, + QueueTimeChart, + CycleTimeChart, + WIPAgeChart, + IssueRateChart, + ThroughputChart, + ReliabilityHeatmap, +} from "@/components/analytics/QRMDashboardCharts"; +import { Loader2 } from "lucide-react"; + +const QRMDashboard = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [dateRange, setDateRange] = useState(30); + + const { data: metrics, isLoading, refetch } = useQRMDashboardMetrics(dateRange); + + if (isLoading || !metrics) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

{t("qrm.dashboardTitle")}

+

+ {t("qrm.dashboardSubtitle")} +

+
+
+
+ + +
+
+ + {/* Dashboard Grid */} +
+ + {/* Row 1: Outcome Metrics */} +
+ navigate("/admin/jobs?status=completed")} + > + + {t("qrm.otp.title")} + + + + + +
+
+ navigate("/admin/jobs?status=completed")} + > + + {t("qrm.mct.title")} + + + + + +
+ + {/* Row 2: Flow Health */} +
+ navigate("/admin/jobs?status=active")} + > + + {t("qrm.wipAge.title")} + {metrics.wipAge.totalWip} + + + + + +
+
+ navigate("/admin/operations")} + > + + {t("qrm.queueTime.title")} + + + + + +
+ + {/* Row 3: Operational Detail */} +
+ navigate("/admin/operations")} + > + + {t("qrm.cycleTime.title")} + + + + + +
+
+ navigate("/admin/issues")} + > + + {t("qrm.issueRate.title")} + {metrics.issueRate.totalIssues} + + + + + +
+
+ navigate("/admin/capacity")} + > + + {t("qrm.throughput.title")} + + + + + +
+ + {/* Row 4: Reliability Heatmap */} +
+ navigate("/admin/operations")} + > + + {t("qrm.reliability.title")} + + + + + +
+ +
+
+ ); +}; + +export default QRMDashboard; diff --git a/src/pages/admin/analytics/ReliabilityAnalytics.tsx b/src/pages/admin/analytics/ReliabilityAnalytics.tsx new file mode 100644 index 00000000..9c3c9152 --- /dev/null +++ b/src/pages/admin/analytics/ReliabilityAnalytics.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { ReliabilityCharts } from "@/components/analytics/ReliabilityCharts"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { ROUTES } from "@/routes"; + +const ReliabilityAnalytics = () => { + const navigate = useNavigate(); + + return ( +
+
+ +
+

Production Reliability

+

+ Analysis of production start dates and schedule adherence. +

+
+
+ + +
+ ); +}; + +export default ReliabilityAnalytics; diff --git a/src/routes.ts b/src/routes.ts index 183616a8..6cb32ada 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -35,6 +35,14 @@ export const ROUTES = { MCP_KEYS: "/admin/config/mcp-keys", WEBHOOKS: "/admin/config/webhooks", MCP_SERVER: "/admin/config/mcp-server", + }, + + ANALYTICS: { + ROOT: "/admin/analytics", + OEE: "/admin/analytics/oee", + RELIABILITY: "/admin/analytics/reliability", + QRM: "/admin/analytics/qrm", + QRM_DASHBOARD: "/admin/analytics/qrm-dashboard", } },