Bereinige und vereinfache Installations-Skripte
- Entferne alle überflüssigen Installations- und Konfigurationsskripte - Erstelle zwei vereinfachte Docker-Installationsskripte: - install-frontend.sh für Frontend-Installation - install-backend.sh für Backend-Installation - Verbessere Frontend Dockerfile mit besserer Unterstützung für native Dependencies - Aktualisiere Backend Dockerfile für automatische DB-Initialisierung - Korrigiere TypeScript-Fehler in personalized-cards.tsx - Erstelle env.ts für Umgebungsvariablen-Verwaltung - Füge ausführliche Installationsanleitung in INSTALL.md hinzu - Konfiguriere Docker-Compose für Host-Netzwerkmodus - Erweitere Dockerfiles mit Healthchecks für bessere Zuverlässigkeit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
0
packages/reservation-platform/src/app/admin/about/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/about/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/admin-sidebar.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/admin-sidebar.tsx
Executable file → Normal file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { BarChart } from "@tremor/react";
|
||||
|
||||
interface AbortReasonsBarChartProps {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: temporary fix
|
||||
data: any[];
|
||||
}
|
||||
|
||||
export function AbortReasonsBarChart(props: AbortReasonsBarChartProps) {
|
||||
const { data } = props;
|
||||
|
||||
const dataFormatter = (number: number) => Intl.NumberFormat("de-DE").format(number).toString();
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
className="mt-6"
|
||||
data={data}
|
||||
index="name"
|
||||
categories={["Anzahl"]}
|
||||
colors={["blue"]}
|
||||
valueFormatter={dataFormatter}
|
||||
yAxisWidth={48}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { DonutChart, Legend } from "@tremor/react";
|
||||
|
||||
const dataFormatter = (number: number) => Intl.NumberFormat("de-DE").format(number).toString();
|
||||
|
||||
interface LoadFactorChartProps {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: temp. fix
|
||||
data: any[];
|
||||
}
|
||||
export function LoadFactorChart(props: LoadFactorChartProps) {
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<DonutChart data={data} variant="donut" colors={["green", "yellow"]} valueFormatter={dataFormatter} />
|
||||
<Legend categories={["Frei", "Belegt"]} colors={["green", "yellow"]} className="max-w-xs" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
"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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Abbruchgründe</CardTitle>
|
||||
<CardDescription>Häufigkeit der Abbruchgründe für Druckaufträge</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="abortReason"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value}
|
||||
/>
|
||||
<YAxis tickFormatter={(value) => `${value}`} />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="count" fill="hsl(var(--chart-1))" radius={8}>
|
||||
<LabelList
|
||||
position="top"
|
||||
offset={12}
|
||||
className="fill-foreground"
|
||||
fontSize={12}
|
||||
formatter={(value: number) => `${value}`}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
"use client";
|
||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts";
|
||||
|
||||
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";
|
||||
|
||||
export const description = "Ein Säulendiagramm zur Darstellung der Fehlerrate";
|
||||
|
||||
interface PrinterErrorRateChartProps {
|
||||
printerErrorRate: PrinterErrorRate[];
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
errorRate: {
|
||||
label: "Fehlerrate",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function PrinterErrorRateChart({ printerErrorRate }: PrinterErrorRateChartProps) {
|
||||
// Transform data to fit the chart structure
|
||||
const chartData = printerErrorRate.map((printer) => ({
|
||||
printer: printer.name,
|
||||
errorRate: printer.errorRate,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Fehlerrate</CardTitle>
|
||||
<CardDescription>Fehlerrate der Drucker in Prozent</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="printer"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value}
|
||||
/>
|
||||
<YAxis tickFormatter={(value) => `${value}%`} />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="errorRate" fill="hsl(var(--chart-1))" radius={8}>
|
||||
<LabelList
|
||||
position="top"
|
||||
offset={12}
|
||||
className="fill-foreground"
|
||||
fontSize={12}
|
||||
formatter={(value: number) => `${value}%`}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
"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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Prognostizierte Nutzung pro Wochentag</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer className="h-64 w-full" config={chartConfig}>
|
||||
<AreaChart accessibilityLayer data={chartData} margin={{ left: 12, right: 12, top: 12 }}>
|
||||
<CartesianGrid vertical={true} />
|
||||
<XAxis dataKey="day" type="category" tickLine={true} tickMargin={10} axisLine={false} />
|
||||
<YAxis type="number" dataKey="usage" tickLine={false} tickMargin={10} axisLine={false} />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Area
|
||||
dataKey="usage"
|
||||
type="step"
|
||||
fill="hsl(var(--chart-1))"
|
||||
fillOpacity={0.4}
|
||||
stroke="hsl(var(--chart-1))"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Zeigt die prognostizierte Nutzungszeit pro Wochentag in Minuten.
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Besten Tage zur Wartung: {bestMaintenanceDays(forecastData)}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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(", ");
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TrendingUp } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Label, Pie, PieChart } 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 = "Nutzung des Druckers";
|
||||
|
||||
interface ComponentProps {
|
||||
data: {
|
||||
printerId: string;
|
||||
utilizationPercentage: number;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
const chartConfig = {} satisfies ChartConfig;
|
||||
|
||||
export function PrinterUtilizationChart({ data }: ComponentProps) {
|
||||
const totalUtilization = React.useMemo(() => data.utilizationPercentage, [data]);
|
||||
const dataWithColor = {
|
||||
...data,
|
||||
fill: "rgb(34 197 94)",
|
||||
};
|
||||
const free = {
|
||||
printerId: "-",
|
||||
utilizationPercentage: 1 - data.utilizationPercentage,
|
||||
name: "(Frei)",
|
||||
fill: "rgb(212 212 212)",
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>{data.name}</CardTitle>
|
||||
<CardDescription>Nutzung des ausgewählten Druckers</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie
|
||||
data={[dataWithColor, free]}
|
||||
dataKey="utilizationPercentage"
|
||||
nameKey="name"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
|
||||
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-3xl font-bold">
|
||||
{(totalUtilization * 100).toFixed(2)}%
|
||||
</tspan>
|
||||
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground">
|
||||
Gesamt-Nutzung
|
||||
</tspan>
|
||||
</text>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Übersicht der Nutzung <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">Aktuelle Auslastung des Druckers</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } 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 Balkendiagramm mit Beschriftung";
|
||||
|
||||
interface PrintVolumes {
|
||||
today: number;
|
||||
thisWeek: number;
|
||||
thisMonth: number;
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
volume: {
|
||||
label: "Volumen",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
interface PrinterVolumeChartProps {
|
||||
printerVolume: PrintVolumes;
|
||||
}
|
||||
|
||||
export function PrinterVolumeChart({ printerVolume }: PrinterVolumeChartProps) {
|
||||
const chartData = [
|
||||
{ period: "Heute", volume: printerVolume.today, color: "hsl(var(--chart-1))" },
|
||||
{ period: "Diese Woche", volume: printerVolume.thisWeek, color: "hsl(var(--chart-2))" },
|
||||
{ period: "Diesen Monat", volume: printerVolume.thisMonth, color: "hsl(var(--chart-3))" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Druckvolumen</CardTitle>
|
||||
<CardDescription>Vergleich: Heute, Diese Woche, Diesen Monat</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer className="h-64 w-full" config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="period"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="volume" fill="var(--color-volume)" radius={8}>
|
||||
<LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Zeigt das Druckvolumen für heute, diese Woche und diesen Monat
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { DonutChart, Legend } from "@tremor/react";
|
||||
|
||||
const dataFormatter = (number: number) => Intl.NumberFormat("de-DE").format(number).toString();
|
||||
|
||||
interface PrintJobsDonutProps {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: temp. fix
|
||||
data: any[];
|
||||
}
|
||||
export function PrintJobsDonut(props: PrintJobsDonutProps) {
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<DonutChart data={data} variant="donut" colors={["green", "red", "yellow"]} valueFormatter={dataFormatter} />
|
||||
<Legend
|
||||
categories={["Abgeschlossen", "Abgebrochen", "Ausstehend"]}
|
||||
colors={["green", "red", "yellow"]}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
packages/reservation-platform/src/app/admin/jobs/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/jobs/page.tsx
Executable file → Normal file
6
packages/reservation-platform/src/app/admin/layout.tsx
Executable file → Normal file
6
packages/reservation-platform/src/app/admin/layout.tsx
Executable file → Normal file
@@ -1,20 +1,18 @@
|
||||
import { AdminSidebar } from "@/app/admin/admin-sidebar";
|
||||
import { validateRequest } from "@/server/auth";
|
||||
import { UserRole } from "@/server/auth/permissions";
|
||||
import { IS_NOT, guard } from "@/utils/guard";
|
||||
import { guard, is_not } from "@/utils/heimdall";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface AdminLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminLayout(props: AdminLayoutProps) {
|
||||
const { children } = props;
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
if (guard(user, is_not, UserRole.ADMIN)) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
|
||||
175
packages/reservation-platform/src/app/admin/page.tsx
Executable file → Normal file
175
packages/reservation-platform/src/app/admin/page.tsx
Executable file → Normal file
@@ -1,17 +1,10 @@
|
||||
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 { AbortReasonsBarChart } from "@/app/admin/charts/abort-reasons";
|
||||
import { LoadFactorChart } from "@/app/admin/charts/load-factor";
|
||||
import { PrintJobsDonut } from "@/app/admin/charts/printjobs-donut";
|
||||
import { DataCard } from "@/components/data-card";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { db } from "@/server/db";
|
||||
import { calculatePrinterErrorRate } from "@/utils/analytics/error-rate";
|
||||
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";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -21,100 +14,114 @@ export const metadata: Metadata = {
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const currentDate = new Date();
|
||||
|
||||
const lastMonth = new Date();
|
||||
lastMonth.setDate(currentDate.getDate() - 31);
|
||||
const printers = await db.query.printers.findMany({});
|
||||
const printJobs = await db.query.printJobs.findMany({
|
||||
where: (job, { gte }) => gte(job.startAt, lastMonth),
|
||||
const allPrintJobs = await db.query.printJobs.findMany({
|
||||
with: {
|
||||
printer: true,
|
||||
},
|
||||
});
|
||||
if (printJobs.length < 1) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Druckaufträge</CardTitle>
|
||||
<CardDescription>Zurzeit sind keine Druckaufträge verfügbar.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Aktualisieren Sie die Seite oder prüfen Sie später erneut, ob neue Druckaufträge verfügbar sind.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const currentPrintJobs = printJobs.filter((job) => {
|
||||
const totalAmountOfPrintJobs = allPrintJobs.length;
|
||||
|
||||
const now = new Date();
|
||||
const completedPrintJobs = allPrintJobs.filter((job) => {
|
||||
if (job.aborted) return false;
|
||||
const endAt = new Date(job.startAt).getTime() + job.durationInMinutes * 1000 * 60;
|
||||
return endAt < now.getTime();
|
||||
}).length;
|
||||
const abortedPrintJobs = allPrintJobs.filter((job) => job.aborted).length;
|
||||
const pendingPrintJobs = totalAmountOfPrintJobs - completedPrintJobs - abortedPrintJobs;
|
||||
|
||||
const endAt = job.startAt.getTime() + job.durationInMinutes * 1000 * 60;
|
||||
const abortedPrintJobsReasons = Object.entries(
|
||||
allPrintJobs.reduce((accumulator: Record<string, number>, job) => {
|
||||
if (job.aborted && job.abortReason) {
|
||||
if (!accumulator[job.abortReason]) {
|
||||
accumulator[job.abortReason] = 1;
|
||||
} else {
|
||||
accumulator[job.abortReason]++;
|
||||
}
|
||||
}
|
||||
return accumulator;
|
||||
}, {}),
|
||||
).map(([name, count]) => ({ name, Anzahl: count }));
|
||||
|
||||
return endAt > currentDate.getTime();
|
||||
const mostAbortedPrinter = allPrintJobs.reduce((prev, current) => (prev.aborted > current.aborted ? prev : current));
|
||||
|
||||
const mostUsedPrinter = allPrintJobs.reduce((prev, current) =>
|
||||
prev.durationInMinutes > current.durationInMinutes ? prev : current,
|
||||
);
|
||||
|
||||
const allPrinters = await db.query.printers.findMany();
|
||||
|
||||
const freePrinters = allPrinters.filter((printer) => {
|
||||
const jobs = allPrintJobs.filter((job) => job.printerId === printer.id);
|
||||
const now = new Date();
|
||||
const inUse = jobs.some((job) => {
|
||||
const endAt = new Date(job.startAt).getTime() + job.durationInMinutes * 1000 * 60;
|
||||
return endAt > now.getTime();
|
||||
});
|
||||
return !inUse;
|
||||
});
|
||||
const occupiedPrinters = currentPrintJobs.map((job) => job.printer.id);
|
||||
const freePrinters = printers.filter((printer) => !occupiedPrinters.includes(printer.id));
|
||||
const printerUtilization = calculatePrinterUtilization(printJobs);
|
||||
const printerVolume = calculatePrintVolumes(printJobs);
|
||||
const printerAbortReasons = calculateAbortReasonsCount(printJobs);
|
||||
const printerErrorRate = calculatePrinterErrorRate(printJobs);
|
||||
const printerForecast = forecastPrinterUsage(printJobs);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultValue={"@general"} className="flex flex-col gap-4 items-start">
|
||||
<TabsList className="bg-neutral-100 w-full py-6">
|
||||
<TabsTrigger value="@general">Allgemein</TabsTrigger>
|
||||
<TabsTrigger value="@capacity">Druckerauslastung</TabsTrigger>
|
||||
<TabsTrigger value="@report">Fehlerberichte</TabsTrigger>
|
||||
<TabsTrigger value="@forecasts">Prognosen</TabsTrigger>
|
||||
{allPrinters.map((printer) => (
|
||||
<TabsTrigger key={printer.id} value={printer.id}>
|
||||
{printer.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsContent value="@general" className="w-full">
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||
<div className="w-full col-span-2">
|
||||
<DataCard
|
||||
title="Aktuelle Auslastung"
|
||||
value={`${Math.round((occupiedPrinters.length / (freePrinters.length + occupiedPrinters.length)) * 100)}%`}
|
||||
icon={"Percent"}
|
||||
/>
|
||||
</div>
|
||||
<DataCard title="Aktive Drucker" value={occupiedPrinters.length} icon={"Rotate3d"} />
|
||||
<DataCard title="Freie Drucker" value={freePrinters.length} icon={"PowerOff"} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="@capacity" className="w-full">
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||
<div className="w-full col-span-2">
|
||||
<PrinterVolumeChart printerVolume={printerVolume} />
|
||||
</div>
|
||||
{printerUtilization.map((data) => (
|
||||
<PrinterUtilizationChart key={data.printerId} data={data} />
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="@report" className="w-full">
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||
<div className="w-full col-span-2">
|
||||
<PrinterErrorRateChart printerErrorRate={printerErrorRate} />
|
||||
</div>
|
||||
<div className="w-full col-span-2">
|
||||
<AbortReasonCountChart abortReasonCount={printerAbortReasons} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="@forecasts" className="w-full">
|
||||
<div className="flex flex-col lg:grid lg:grid-cols-2 gap-4">
|
||||
<div className="w-full col-span-2">
|
||||
<ForecastPrinterUsageChart
|
||||
forecastData={printerForecast.map((usageMinutes, index) => ({
|
||||
day: index,
|
||||
usageMinutes,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<DataCard title="Drucker mit meisten Reservierungen" value={mostUsedPrinter.printer.name} icon="Printer" />
|
||||
<DataCard title="Drucker mit meisten Abbrüchen" value={mostAbortedPrinter.printer.name} icon="Printer" />
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Druckaufträge</CardTitle>
|
||||
<CardDescription>nach Status</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PrintJobsDonut
|
||||
data={[
|
||||
{ name: "Abgeschlossen", value: completedPrintJobs },
|
||||
{ name: "Abgebrochen", value: abortedPrintJobs },
|
||||
{ name: "Ausstehend", value: pendingPrintJobs },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full ">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Auslastung: <span>{((1 - freePrinters.length / allPrinters.length) * 100).toFixed(2)}%</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LoadFactorChart
|
||||
data={[
|
||||
{ name: "Frei", value: freePrinters.length },
|
||||
{ name: "Belegt", value: allPrinters.length - freePrinters.length },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Abgebrochene Druckaufträge nach Abbruchgrund</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AbortReasonsBarChart data={abortedPrintJobsReasons} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
{allPrinters.map((printer) => (
|
||||
<TabsContent key={printer.id} value={printer.id}>
|
||||
{printer.description}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
|
||||
0
packages/reservation-platform/src/app/admin/printers/columns.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/printers/columns.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/printers/data-table.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/printers/data-table.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/printers/dialogs/create-printer.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/printers/dialogs/create-printer.tsx
Executable file → Normal file
8
packages/reservation-platform/src/app/admin/printers/dialogs/delete-printer.tsx
Executable file → Normal file
8
packages/reservation-platform/src/app/admin/printers/dialogs/delete-printer.tsx
Executable file → Normal file
@@ -29,13 +29,7 @@ export function DeletePrinterDialog(props: DeletePrinterDialogProps) {
|
||||
description: "Drucker wird gelöscht...",
|
||||
});
|
||||
try {
|
||||
const result = await deletePrinter(printerId);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
await deletePrinter(printerId);
|
||||
toast({
|
||||
description: "Drucker wurde gelöscht.",
|
||||
});
|
||||
|
||||
0
packages/reservation-platform/src/app/admin/printers/dialogs/edit-printer.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/printers/dialogs/edit-printer.tsx
Executable file → Normal file
16
packages/reservation-platform/src/app/admin/printers/form.tsx
Executable file → Normal file
16
packages/reservation-platform/src/app/admin/printers/form.tsx
Executable file → Normal file
@@ -57,17 +57,11 @@ export function PrinterForm(props: PrinterFormProps) {
|
||||
|
||||
// Update
|
||||
try {
|
||||
const result = await updatePrinter(printer.id, {
|
||||
await updatePrinter(printer.id, {
|
||||
description: values.description,
|
||||
name: values.name,
|
||||
status: values.status,
|
||||
});
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
|
||||
@@ -96,17 +90,11 @@ export function PrinterForm(props: PrinterFormProps) {
|
||||
|
||||
// Create
|
||||
try {
|
||||
const result = await createPrinter({
|
||||
await createPrinter({
|
||||
description: values.description,
|
||||
name: values.name,
|
||||
status: values.status,
|
||||
});
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
|
||||
|
||||
0
packages/reservation-platform/src/app/admin/printers/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/printers/page.tsx
Executable file → Normal file
2
packages/reservation-platform/src/app/admin/settings/download/route.ts
Executable file → Normal file
2
packages/reservation-platform/src/app/admin/settings/download/route.ts
Executable file → Normal file
@@ -1,7 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
return new Response(fs.readFileSync("./db/sqlite.db"));
|
||||
}
|
||||
|
||||
0
packages/reservation-platform/src/app/admin/settings/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/settings/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/columns.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/columns.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/data-table.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/data-table.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/dialog.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/dialog.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/form.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/form.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/admin/users/page.tsx
Executable file → Normal file
7
packages/reservation-platform/src/app/api/job/[jobId]/remaining-time/route.ts
Executable file → Normal file
7
packages/reservation-platform/src/app/api/job/[jobId]/remaining-time/route.ts
Executable file → Normal file
@@ -2,19 +2,12 @@ import { db } from "@/server/db";
|
||||
import { printJobs } from "@/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface RemainingTimeRouteProps {
|
||||
params: {
|
||||
jobId: string;
|
||||
};
|
||||
}
|
||||
export async function GET(request: Request, { params }: RemainingTimeRouteProps) {
|
||||
// Trying to fix build error in container...
|
||||
if (params.jobId === undefined) {
|
||||
return Response.json({});
|
||||
}
|
||||
|
||||
// Get the job details
|
||||
const jobDetails = await db.query.printJobs.findFirst({
|
||||
where: eq(printJobs.id, params.jobId),
|
||||
|
||||
21
packages/reservation-platform/src/app/api/printers/route.ts
Executable file → Normal file
21
packages/reservation-platform/src/app/api/printers/route.ts
Executable file → Normal file
@@ -1,22 +1,7 @@
|
||||
import { backendApi } from "@/utils/fetch";
|
||||
import { mapBackendPrinterToFrontend } from "@/utils/backend-types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
import { getPrinters } from "@/server/actions/printers";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Hole Drucker vom Backend statt aus der lokalen Datenbank
|
||||
const backendPrinters = await backendApi.printers.getAll();
|
||||
|
||||
// Transformiere die Backend-Daten ins Frontend-Format
|
||||
const printers = backendPrinters.map(mapBackendPrinterToFrontend);
|
||||
const printers = await getPrinters();
|
||||
|
||||
return Response.json(printers);
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Abrufen der Drucker vom Backend:", error);
|
||||
return Response.json(
|
||||
{ error: "Fehler beim Abrufen der Drucker vom Backend" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return Response.json(printers);
|
||||
}
|
||||
|
||||
50
packages/reservation-platform/src/app/auth/login/callback/route.ts
Executable file → Normal file
50
packages/reservation-platform/src/app/auth/login/callback/route.ts
Executable file → Normal file
@@ -7,30 +7,15 @@ import { eq } from "drizzle-orm";
|
||||
import { generateIdFromEntropySize } from "lucia";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface GithubEmailResponse {
|
||||
email: string;
|
||||
primary: boolean;
|
||||
verified: boolean;
|
||||
visibility: string;
|
||||
}
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
const storedState = cookies().get("github_oauth_state")?.value ?? null;
|
||||
if (!code || !state || !storedState || state !== storedState) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status_text: "Something is wrong",
|
||||
data: { code, state, storedState },
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -42,16 +27,7 @@ export async function GET(request: Request): Promise<Response> {
|
||||
});
|
||||
const githubUser: GitHubUserResult = await githubUserResponse.json();
|
||||
|
||||
// Sometimes email can be null in the user query.
|
||||
if (githubUser.email === null || githubUser.email === undefined) {
|
||||
const githubEmailResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user/emails", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
});
|
||||
const githubUserEmail: GithubEmailResponse[] = await githubEmailResponse.json();
|
||||
githubUser.email = githubUserEmail[0].email;
|
||||
}
|
||||
// Replace this with your own DB client.
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.github_id, githubUser.id),
|
||||
});
|
||||
@@ -80,10 +56,7 @@ export async function GET(request: Request): Promise<Response> {
|
||||
|
||||
const session = await lucia.createSession(userId, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
cookies().set(sessionCookie.name, sessionCookie.value, {
|
||||
...sessionCookie.attributes,
|
||||
secure: false, // Else cookie does not get set cause IT has not provided us an SSL certificate yet
|
||||
});
|
||||
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
@@ -91,18 +64,13 @@ export async function GET(request: Request): Promise<Response> {
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
// the specific error message depends on the provider
|
||||
if (e instanceof OAuth2RequestError) {
|
||||
// invalid code
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status_text: "Invalid code",
|
||||
error: JSON.stringify(e),
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
|
||||
8
packages/reservation-platform/src/app/auth/login/route.ts
Executable file → Normal file
8
packages/reservation-platform/src/app/auth/login/route.ts
Executable file → Normal file
@@ -2,18 +2,14 @@ import { github } from "@/server/auth/oauth";
|
||||
import { generateState } from "arctic";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const state = generateState();
|
||||
const url = await github.createAuthorizationURL(state, {
|
||||
scopes: ["user"],
|
||||
});
|
||||
const url = await github.createAuthorizationURL(state);
|
||||
const ONE_HOUR = 60 * 60;
|
||||
|
||||
cookies().set("github_oauth_state", state, {
|
||||
path: "/",
|
||||
secure: false, //process.env.NODE_ENV === "production", -- can't be used until SSL certificate is provided by IT
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: true,
|
||||
maxAge: ONE_HOUR,
|
||||
sameSite: "lax",
|
||||
|
||||
BIN
packages/reservation-platform/src/app/favicon.ico
Executable file → Normal file
BIN
packages/reservation-platform/src/app/favicon.ico
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 25 KiB |
106
packages/reservation-platform/src/app/globals.css
Executable file → Normal file
106
packages/reservation-platform/src/app/globals.css
Executable file → Normal file
@@ -2,60 +2,76 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--muted: 0 0% 90.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.75rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
8
packages/reservation-platform/src/app/job/[jobId]/cancel-form.tsx
Executable file → Normal file
8
packages/reservation-platform/src/app/job/[jobId]/cancel-form.tsx
Executable file → Normal file
@@ -52,13 +52,7 @@ export function CancelForm(props: CancelFormProps) {
|
||||
description: "Druckauftrag wird abgebrochen...",
|
||||
});
|
||||
try {
|
||||
const result = await abortPrintJob(jobId, values.abortReason);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
await abortPrintJob(jobId, values.abortReason);
|
||||
setOpen(false);
|
||||
toast({
|
||||
description: "Druckauftrag wurde abgebrochen.",
|
||||
|
||||
8
packages/reservation-platform/src/app/job/[jobId]/edit-comments.tsx
Executable file → Normal file
8
packages/reservation-platform/src/app/job/[jobId]/edit-comments.tsx
Executable file → Normal file
@@ -17,13 +17,7 @@ export function EditComments(props: EditCommentsProps) {
|
||||
|
||||
const debounced = useDebouncedCallback(async (value) => {
|
||||
try {
|
||||
const result = await updatePrintComments(jobId, value);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
await updatePrintComments(jobId, value);
|
||||
toast({
|
||||
description: "Anmerkungen wurden gespeichert.",
|
||||
});
|
||||
|
||||
9
packages/reservation-platform/src/app/job/[jobId]/extend-form.tsx
Executable file → Normal file
9
packages/reservation-platform/src/app/job/[jobId]/extend-form.tsx
Executable file → Normal file
@@ -53,14 +53,7 @@ export function ExtendForm(props: ExtendFormProps) {
|
||||
description: "Druckauftrag wird verlängert...",
|
||||
});
|
||||
try {
|
||||
const result = await extendPrintJob(jobId, values.minutes, values.hours);
|
||||
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
await extendPrintJob(jobId, values.minutes, values.hours);
|
||||
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
|
||||
8
packages/reservation-platform/src/app/job/[jobId]/finish-form.tsx
Executable file → Normal file
8
packages/reservation-platform/src/app/job/[jobId]/finish-form.tsx
Executable file → Normal file
@@ -27,13 +27,7 @@ export function FinishForm(props: FinishFormProps) {
|
||||
description: "Druckauftrag wird abgeschlossen...",
|
||||
});
|
||||
try {
|
||||
const result = await earlyFinishPrintJob(jobId);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
await earlyFinishPrintJob(jobId);
|
||||
toast({
|
||||
description: "Druckauftrag wurde abgeschlossen.",
|
||||
});
|
||||
|
||||
2
packages/reservation-platform/src/app/job/[jobId]/page.tsx
Executable file → Normal file
2
packages/reservation-platform/src/app/job/[jobId]/page.tsx
Executable file → Normal file
@@ -36,7 +36,7 @@ export default async function JobDetailsPage(props: JobDetailsPageProps) {
|
||||
});
|
||||
|
||||
if (!jobDetails) {
|
||||
return <div>Druckauftrag wurde nicht gefunden.</div>;
|
||||
return <div>Job not found</div>;
|
||||
}
|
||||
|
||||
const jobIsOnGoing = new Date(jobDetails.startAt).getTime() + jobDetails.durationInMinutes * 60 * 1000 > Date.now();
|
||||
|
||||
11
packages/reservation-platform/src/app/layout.tsx
Executable file → Normal file
11
packages/reservation-platform/src/app/layout.tsx
Executable file → Normal file
@@ -1,8 +1,15 @@
|
||||
import { Header } from "@/components/header";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { cn } from "@/utils/styles";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import "@/app/globals.css";
|
||||
import { Inter as FontSans } from "next/font/google";
|
||||
|
||||
const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
@@ -16,15 +23,13 @@ interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default function RootLayout(props: RootLayoutProps) {
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<html lang="de" suppressHydrationWarning>
|
||||
<head />
|
||||
<body className={"min-h-dvh bg-neutral-200 font-sans antialiased"}>
|
||||
<body className={cn("min-h-dvh bg-muted font-sans antialiased", fontSans.variable)}>
|
||||
<Header />
|
||||
<main className="flex-grow max-w-screen-2xl w-full mx-auto flex flex-col p-8 gap-4 text-foreground">
|
||||
{children}
|
||||
|
||||
0
packages/reservation-platform/src/app/my/jobs/columns.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/my/jobs/columns.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/my/jobs/data-table.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/my/jobs/data-table.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/my/profile/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/my/profile/page.tsx
Executable file → Normal file
@@ -1,11 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Nicht gefunden</h2>
|
||||
<p>Die angefragte Seite konnte nicht gefunden werden.</p>
|
||||
<Link href="/">Zurück zur Startseite</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
packages/reservation-platform/src/app/page.tsx
Executable file → Normal file
15
packages/reservation-platform/src/app/page.tsx
Executable file → Normal file
@@ -1,12 +1,11 @@
|
||||
import { columns } from "@/app/my/jobs/columns";
|
||||
import { JobsTable } from "@/app/my/jobs/data-table";
|
||||
import { DynamicPrinterCards } from "@/components/dynamic-printer-cards";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { validateRequest } from "@/server/auth";
|
||||
import { db } from "@/server/db";
|
||||
import { printJobs } from "@/server/db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { BoxesIcon, NewspaperIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -44,10 +43,8 @@ export default async function HomePage() {
|
||||
{/* NEEDS TO BE FIXED FOR A NEW / EMPTY USER {isLoggedIn && <PersonalizedCards />} */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex flex-row items-center gap-x-1">
|
||||
<BoxesIcon className="w-5 h-5" />
|
||||
<span className="text-lg">Druckerbelegung</span>
|
||||
</CardTitle>
|
||||
<CardTitle>Druckerbelegung</CardTitle>
|
||||
<CardDescription>({printers.length} Verfügbar)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<DynamicPrinterCards user={user} />
|
||||
@@ -56,10 +53,8 @@ export default async function HomePage() {
|
||||
{userIsLoggedIn && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex flex-row items-center gap-x-1">
|
||||
<NewspaperIcon className="w-5 h-5" />
|
||||
<span className="text-lg">Druckaufträge</span>
|
||||
</CardTitle>
|
||||
<CardTitle>Druckaufträge</CardTitle>
|
||||
<CardDescription>Deine aktuellen Druckaufträge</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<JobsTable columns={columns} data={jobs} />
|
||||
|
||||
46
packages/reservation-platform/src/app/printer/[printerId]/reserve/form.tsx
Executable file → Normal file
46
packages/reservation-platform/src/app/printer/[printerId]/reserve/form.tsx
Executable file → Normal file
@@ -1,7 +1,15 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogClose } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
@@ -9,7 +17,6 @@ import { createPrintJob } from "@/server/actions/printJobs";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CalendarPlusIcon, XCircleIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { If, Then } from "react-if";
|
||||
import { z } from "zod";
|
||||
@@ -34,7 +41,6 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
const { userId, printerId, isDialog } = props;
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [isLocked, setLocked] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -46,25 +52,13 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (!isLocked) {
|
||||
setLocked(true);
|
||||
setTimeout(() => {
|
||||
setLocked(false);
|
||||
}, 1000 * 5);
|
||||
} else {
|
||||
toast({
|
||||
description: "Bitte warte ein wenig, bevor du eine weitere Reservierung tätigst...",
|
||||
variant: "default",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.hours === 0 && values.minutes === 0) {
|
||||
form.setError("hours", {
|
||||
message: "",
|
||||
});
|
||||
form.setError("minutes", {
|
||||
message: "Die Dauer des Druckauftrags muss mindestens 1 Minute betragen.",
|
||||
message:
|
||||
"Die Dauer des Druckauftrags muss mindestens 1 Minute betragen.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -76,12 +70,6 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
userId: userId,
|
||||
printerId: printerId,
|
||||
});
|
||||
if (typeof jobId === "object") {
|
||||
toast({
|
||||
description: jobId.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
router.push(`/job/${jobId}`);
|
||||
} catch (error) {
|
||||
@@ -140,8 +128,9 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
<Textarea placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
In dieses Feld kannst du Anmerkungen zu deinem Druckauftrag hinzufügen. Sie können beispielsweise
|
||||
Informationen über das Druckmaterial, die Druckqualität oder die Farbe enthalten.
|
||||
In dieses Feld kannst du Anmerkungen zu deinem Druckauftrag
|
||||
hinzufügen. Sie können beispielsweise Informationen über das
|
||||
Druckmaterial, die Druckqualität oder die Farbe enthalten.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -151,14 +140,17 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
<If condition={isDialog}>
|
||||
<Then>
|
||||
<DialogClose asChild>
|
||||
<Button variant={"secondary"} className="gap-2 flex items-center">
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="gap-2 flex items-center"
|
||||
>
|
||||
<XCircleIcon className="w-4 h-4" />
|
||||
<span>Abbrechen</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</Then>
|
||||
</If>
|
||||
<Button type="submit" className="gap-2 flex items-center" disabled={isLocked}>
|
||||
<Button type="submit" className="gap-2 flex items-center">
|
||||
<CalendarPlusIcon className="w-4 h-4" />
|
||||
<span>Reservieren</span>
|
||||
</Button>
|
||||
|
||||
0
packages/reservation-platform/src/app/printer/[printerId]/reserve/page.tsx
Executable file → Normal file
0
packages/reservation-platform/src/app/printer/[printerId]/reserve/page.tsx
Executable file → Normal file
Reference in New Issue
Block a user