diff --git a/packages/reservation-platform/src/app/admin/charts/printer-error-chart.tsx b/packages/reservation-platform/src/app/admin/charts/printer-error-chart.tsx new file mode 100644 index 0000000..0749c8f --- /dev/null +++ b/packages/reservation-platform/src/app/admin/charts/printer-error-chart.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; +import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts"; + +export const description = "Ein Säulendiagramm zur Darstellung der Abbruchgründe und ihrer Häufigkeit"; + +interface AbortReasonCountChartProps { + abortReasonCount: { + abortReason: string; + count: number; + }[]; +} + +const chartConfig = { + abortReason: { + label: "Abbruchgrund", + }, +} satisfies ChartConfig; + +export function AbortReasonCountChart({ abortReasonCount }: AbortReasonCountChartProps) { + // Transform data to fit the chart structure + const chartData = abortReasonCount.map((reason) => ({ + abortReason: reason.abortReason, + count: reason.count, + })); + + return ( + + + Abbruchgründe + Häufigkeit der Abbruchgründe für Druckaufträge + + + + + + value} + /> + `${value}`} /> + } /> + + `${value}`} + /> + + + + + + ); +} diff --git a/packages/reservation-platform/src/app/admin/charts/printer-error.tsx b/packages/reservation-platform/src/app/admin/charts/printer-error-rate.tsx similarity index 79% rename from packages/reservation-platform/src/app/admin/charts/printer-error.tsx rename to packages/reservation-platform/src/app/admin/charts/printer-error-rate.tsx index f2364e1..22ffc7a 100644 --- a/packages/reservation-platform/src/app/admin/charts/printer-error.tsx +++ b/packages/reservation-platform/src/app/admin/charts/printer-error-rate.tsx @@ -1,9 +1,7 @@ "use client"; - -import { TrendingUp } from "lucide-react"; import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; import type { PrinterErrorRate } from "@/utils/analytics/error-rate"; @@ -63,12 +61,6 @@ export function PrinterErrorRateChart({ printerErrorRate }: PrinterErrorRateChar - -
- Fehlerratenanalyse abgeschlossen -
-
Zeigt die Fehlerrate für jeden Drucker
-
); } diff --git a/packages/reservation-platform/src/app/admin/charts/printer-forecast.tsx b/packages/reservation-platform/src/app/admin/charts/printer-forecast.tsx new file mode 100644 index 0000000..d8b27e1 --- /dev/null +++ b/packages/reservation-platform/src/app/admin/charts/printer-forecast.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; +import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; + +export const description = "Ein Bereichsdiagramm zur Darstellung der prognostizierten Nutzung pro Wochentag"; + +interface ForecastData { + day: number; // 0 for Sunday, 1 for Monday, ..., 6 for Saturday + usageMinutes: number; +} + +interface ForecastChartProps { + forecastData: ForecastData[]; +} + +const chartConfig = { + usage: { + label: "Prognostizierte Nutzung", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; + +const daysOfWeek = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; + +export function ForecastPrinterUsageChart({ forecastData }: ForecastChartProps) { + // Transform and slice data to fit the chart structure + const chartData = forecastData.map((data) => ({ + //slice(1, forecastData.length - 1). + day: daysOfWeek[data.day], // Map day number to weekday name + usage: data.usageMinutes, + })); + + return ( + + + Prognostizierte Nutzung pro Wochentag + + + + + + + + } /> + + + + + +
+ Zeigt die prognostizierte Nutzungszeit pro Wochentag in Minuten. +
+
+ Besten Tage zur Wartung: {bestMaintenanceDays(forecastData)} +
+
+
+ ); +} + +function bestMaintenanceDays(forecastData: ForecastData[]) { + const sortedData = forecastData.map((a) => a).sort((a, b) => a.usageMinutes - b.usageMinutes); // Sort ascending + + const q1Index = Math.floor(sortedData.length * 0.33); + const q1 = sortedData[q1Index].usageMinutes; // First quartile (Q1) value + + const filteredData = sortedData.filter((data) => data.usageMinutes <= q1); + + return filteredData + .map((data) => { + const days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]; + return days[data.day]; + }) + .join(", "); +} diff --git a/packages/reservation-platform/src/app/admin/charts/printer-idle.tsx b/packages/reservation-platform/src/app/admin/charts/printer-idle.tsx deleted file mode 100644 index 1d90bb3..0000000 --- a/packages/reservation-platform/src/app/admin/charts/printer-idle.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; -import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; - -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; - -export const description = "Ein horizontales Balkendiagramm zur Darstellung der durchschnittlichen Leerlaufzeit"; - -interface PrinterIdleTime { - printerId: string; - printerName: string; - averageIdleTime: number; // in minutes -} - -interface PrinterIdleTimeChartProps { - printerIdleTime: PrinterIdleTime[]; -} - -const chartConfig = { - idleTime: { - label: "Leerlaufzeit", - }, -} satisfies ChartConfig; - -export function PrinterIdleTimeChart({ printerIdleTime }: PrinterIdleTimeChartProps) { - // Transform data to fit the chart structure - const chartData = printerIdleTime.map((printer) => ({ - printer: printer.printerName, - idleTime: printer.averageIdleTime, - })); - - return ( - - - Leerlaufzeit der Drucker - Durchschnittliche Leerlaufzeit der Drucker in Minuten - - - - - - - value} - /> - } /> - - - - - -
- Zeigt die durchschnittliche Leerlaufzeit für jeden Drucker in Minuten -
-
-
- ); -} diff --git a/packages/reservation-platform/src/app/admin/page.tsx b/packages/reservation-platform/src/app/admin/page.tsx index 7810e4f..755682b 100644 --- a/packages/reservation-platform/src/app/admin/page.tsx +++ b/packages/reservation-platform/src/app/admin/page.tsx @@ -1,5 +1,6 @@ -import { PrinterErrorRateChart } from "@/app/admin/charts/printer-error"; -import { PrinterIdleTimeChart } from "@/app/admin/charts/printer-idle"; +import { AbortReasonCountChart } from "@/app/admin/charts/printer-error-chart"; +import { PrinterErrorRateChart } from "@/app/admin/charts/printer-error-rate"; +import { ForecastPrinterUsageChart } from "@/app/admin/charts/printer-forecast"; import { PrinterUtilizationChart } from "@/app/admin/charts/printer-utilization"; import { PrinterVolumeChart } from "@/app/admin/charts/printer-volume"; import { DataCard } from "@/components/data-card"; @@ -7,7 +8,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { db } from "@/server/db"; import { calculatePrinterErrorRate } from "@/utils/analytics/error-rate"; -import { calculatePrinterIdleTime } from "@/utils/analytics/idle-time"; +import { calculateAbortReasonsCount } from "@/utils/analytics/errors"; +import { forecastPrinterUsage } from "@/utils/analytics/forecast"; import { calculatePrinterUtilization } from "@/utils/analytics/utilization"; import { calculatePrintVolumes } from "@/utils/analytics/volume"; import type { Metadata } from "next"; @@ -55,8 +57,9 @@ export default async function AdminPage() { const freePrinters = printers.filter((printer) => !occupiedPrinters.includes(printer.id)); const printerUtilization = calculatePrinterUtilization(printJobs); const printerVolume = calculatePrintVolumes(printJobs); - const printerIdleTime = calculatePrinterIdleTime(printJobs, printers); + const printerAbortReasons = calculateAbortReasonsCount(printJobs); const printerErrorRate = calculatePrinterErrorRate(printJobs); + const printerForecast = forecastPrinterUsage(printJobs); return ( <> @@ -64,16 +67,27 @@ export default async function AdminPage() { Allgemein Druckerauslastung - Statistiken & Berichte + Fehlerberichte + Prognosen
+
+ +
+
+ +
{printerUtilization.map((data) => ( ))} @@ -81,11 +95,24 @@ export default async function AdminPage() {
- -
+
+ +
+
+
+ +
+
+ ({ + day: index, + usageMinutes, + }))} + /> +
diff --git a/packages/reservation-platform/src/utils/analytics/errors.ts b/packages/reservation-platform/src/utils/analytics/errors.ts new file mode 100644 index 0000000..31e369a --- /dev/null +++ b/packages/reservation-platform/src/utils/analytics/errors.ts @@ -0,0 +1,39 @@ +import type { InferResultType } from "@/utils/drizzle"; + +export interface AbortReasonCount { + abortReason: string; + count: number; +} + +/** + * Calculates the count of each unique abort reason for print jobs. + * + * @param pJobs - Array of print job objects. + * @returns An array of AbortReasonCount objects, each containing the abort reason and its count. + */ +export function calculateAbortReasonsCount(pJobs: InferResultType<"printJobs">[]): AbortReasonCount[] { + if (pJobs.length === 0) { + return []; // No jobs, no data. + } + + // Filter aborted jobs and count each abort reason + const abortReasonsCount = pJobs + .filter((job) => job.aborted && job.abortReason) // Consider only aborted jobs with a reason + .reduce( + (acc, job) => { + const reason = job.abortReason || "Unbekannter Grund"; + if (!acc[reason]) { + acc[reason] = 0; + } + acc[reason]++; + return acc; + }, + {} as Record, + ); + + // Convert the result to an array of AbortReasonCount objects + return Object.entries(abortReasonsCount).map(([abortReason, count]) => ({ + abortReason, + count, + })); +} diff --git a/packages/reservation-platform/src/utils/analytics/forecast.ts b/packages/reservation-platform/src/utils/analytics/forecast.ts new file mode 100644 index 0000000..0aa5268 --- /dev/null +++ b/packages/reservation-platform/src/utils/analytics/forecast.ts @@ -0,0 +1,59 @@ +import type { InferResultType } from "@/utils/drizzle"; + +type UsagePerDay = { + day: number; // 0 (Sunday) to 6 (Saturday) + usageMinutes: number; + dataPoints: number; +}; + +function aggregateUsageByDay(jobs: InferResultType<"printJobs">[]): UsagePerDay[] { + const usagePerDayMap = new Map(); + const usagePerDayDatapointsMap = new Map(); + + for (const job of jobs) { + let remainingDuration = job.durationInMinutes; + const currentStart = new Date(job.startAt); + + while (remainingDuration > 0) { + const day = currentStart.getDay(); + const dataPoints = usagePerDayDatapointsMap.get(day) || 0; + usagePerDayDatapointsMap.set(day, dataPoints + 1); + + // Calculate minutes remaining in the current day + const minutesRemainingInDay = (24 - currentStart.getHours()) * 60 - currentStart.getMinutes(); + const minutesToAdd = Math.min(remainingDuration, minutesRemainingInDay); + + // Update the usage for the current day + const usageMinutes = usagePerDayMap.get(day) || 0; + usagePerDayMap.set(day, usageMinutes + minutesToAdd); + + // Update remaining duration and move to the next day + remainingDuration -= minutesToAdd; + currentStart.setDate(currentStart.getDate() + 1); + currentStart.setHours(0, 0, 0, 0); // Start at the beginning of the next day + } + } + + const usageData: UsagePerDay[] = Array.from({ length: 7 }, (_, day) => ({ + day, + usageMinutes: usagePerDayMap.get(day) || 0, + dataPoints: usagePerDayDatapointsMap.get(day) || 0, + })); + + return usageData; +} + +export function forecastPrinterUsage(jobs: InferResultType<"printJobs">[]): number[] { + const usageData = aggregateUsageByDay(jobs); + console.log(usageData); + const forecasts: number[] = []; + for (const data of usageData) { + let usagePrediction = data.usageMinutes / data.dataPoints; + if (Number.isNaN(usagePrediction)) { + usagePrediction = 0; + } + forecasts.push(Math.round(usagePrediction)); + } + + return forecasts; +} diff --git a/packages/reservation-platform/src/utils/analytics/idle-time.ts b/packages/reservation-platform/src/utils/analytics/idle-time.ts deleted file mode 100644 index 27193e8..0000000 --- a/packages/reservation-platform/src/utils/analytics/idle-time.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { printJobs, printers } from "@/server/db/schema"; -import { endOfMonth, startOfMonth } from "date-fns"; -import type { InferSelectModel } from "drizzle-orm"; - -interface PrinterIdleTime { - printerId: string; - printerName: string; - averageIdleTime: number; // in minutes -} - -/** - * Calculates the average idle time for each printer within the current month. - * - * @param printJobs - Array of print job objects. - * @param printers - Array of printer objects. - * @returns An array of PrinterIdleTime objects with average idle times. - */ -export function calculatePrinterIdleTime( - pJobs: InferSelectModel[], - p: InferSelectModel[], -): PrinterIdleTime[] { - const now = new Date(); - const startOfThisMonth = startOfMonth(now); - const endOfThisMonth = endOfMonth(now); - const totalMinutesInMonth = 60 * 70 * 4; // 60min * 70h (35*2) * 4 Weeks - - const usedTimePerPrinter: Record = pJobs.reduce( - (acc, job) => { - const jobStart = new Date(job.startAt); - if (jobStart >= startOfThisMonth && jobStart <= endOfThisMonth) { - acc[job.printerId] = (acc[job.printerId] || 0) + job.durationInMinutes; - } - return acc; - }, - {} as Record, - ); - - return p.map((printer) => { - const usedTime = usedTimePerPrinter[printer.id] || 0; - const idleTime = totalMinutesInMonth - usedTime; - const averageIdleTime = idleTime < 0 ? 0 : idleTime; // Ensure no negative idle time - - return { - printerId: printer.id, - printerName: printer.name, - averageIdleTime, - }; - }); -} diff --git a/packages/reservation-platform/src/utils/analytics/utilization.ts b/packages/reservation-platform/src/utils/analytics/utilization.ts index f053fce..dfc93b9 100644 --- a/packages/reservation-platform/src/utils/analytics/utilization.ts +++ b/packages/reservation-platform/src/utils/analytics/utilization.ts @@ -15,10 +15,12 @@ export function calculatePrinterUtilization(jobs: InferResultType<"printJobs", { {} as Record, ); - const totalTimeInMinutes = 60 * 70 * 4; // 60 Minutes * 70h * 4 Weeks + const totalTimeInMinutes = 60 * 35 * 3; // 60 Minutes * 35h * 3 Weeks + // 35h Woche, 3 mal in der Woche in TBA const printerUtilizationPercentage = Object.keys(usedTimePerPrinter).map((printerId) => { const usedTime = usedTimePerPrinter[printerId]; + return { printerId, name: printers[printerId],