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:
2025-03-31 14:22:07 +02:00
parent fc62086a50
commit f1541478ad
198 changed files with 1903 additions and 17934 deletions

View File

View File

View File

View File

@@ -1,7 +1,7 @@
import { HeaderNavigation } from "@/components/header/navigation";
import { LoginButton } from "@/components/login-button";
import { LogoutButton } from "@/components/logout-button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -13,7 +13,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { validateRequest } from "@/server/auth";
import { UserRole, hasRole } from "@/server/auth/permissions";
import { StickerIcon, UserIcon, WrenchIcon } from "lucide-react";
import { ScanFaceIcon, StickerIcon, UserIcon, WrenchIcon } from "lucide-react";
import Link from "next/link";
import { If, Then } from "react-if";
@@ -78,7 +78,14 @@ export async function Header() {
</DropdownMenuContent>
</DropdownMenu>
)}
{user == null && <LoginButton />}
{user == null && (
<Button variant={"ghost"} className="gap-2 flex items-center" asChild>
<Link href="/auth/login">
<ScanFaceIcon className="w-4 h-4" />
<span>Anmelden</span>
</Link>
</Button>
)}
</div>
</header>
);

View File

@@ -1,12 +1,10 @@
"use client";
import { cn } from "@/utils/styles";
import { ContactRoundIcon, LayersIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface Site {
name: string;
icon: JSX.Element;
path: string;
}
@@ -14,8 +12,7 @@ export function HeaderNavigation() {
const pathname = usePathname();
const sites: Site[] = [
{
name: "Dashboard",
icon: <LayersIcon className="w-4 h-4" />,
name: "Mein Dashboard",
path: "/",
},
/* {
@@ -24,7 +21,6 @@ export function HeaderNavigation() {
}, */
{
name: "Mein Profil",
icon: <ContactRoundIcon className="w-4 h-4" />,
path: "/my/profile",
},
];
@@ -35,13 +31,12 @@ export function HeaderNavigation() {
<Link
key={site.path}
href={site.path}
className={cn("transition-colors hover:text-neutral-50 flex items-center gap-x-1", {
"text-primary-foreground font-semibold": pathname === site.path,
className={cn("transition-colors hover:text-neutral-50", {
"text-neutral-50": pathname === site.path,
"text-neutral-500": pathname !== site.path,
})}
>
{site.icon}
<span>{site.name}</span>
{site.name}
</Link>
))}
</nav>

View File

@@ -1,37 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
import { ScanFaceIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
export function LoginButton() {
const { toast } = useToast();
const [isLocked, setLocked] = useState(false);
function onClick() {
if (!isLocked) {
toast({
description: "Du wirst angemeldet...",
});
// Prevent multiple clicks because of login delay...
setLocked(true);
setTimeout(() => {
setLocked(false);
}, 1000 * 5);
}
toast({
description: "Bitte warte einen Moment...",
});
}
return (
<Button onClick={onClick} variant={"ghost"} className="gap-2 flex items-center" asChild disabled={isLocked}>
<Link href="/auth/login">
<ScanFaceIcon className="w-4 h-4" />
<span>Anmelden</span>
</Link>
</Button>
);
}

View File

@@ -1,21 +1,12 @@
"use client";
import { useToast } from "@/components/ui/use-toast";
import { logout } from "@/server/actions/authentication/logout";
import { LogOutIcon } from "lucide-react";
import Link from "next/link";
export function LogoutButton() {
const { toast } = useToast();
function onClick() {
toast({
description: "Du wirst nun abgemeldet...",
});
logout();
}
return (
<Link href="/" onClick={onClick} className="flex items-center gap-2">
<Link href="/" onClick={() => logout()} className="flex items-center gap-2">
<LogOutIcon className="w-4 h-4" />
<span>Abmelden</span>
</Link>

View File

@@ -22,29 +22,29 @@ export default async function PersonalizedCards() {
.reduce((acc, curr) => acc + curr.durationInMinutes, 0);
const averagePrintingHoursPerWeek = totalPrintingMinutes / 60 / 52;
const mostUsedPrinters = {printer:{name:'-'}}; /*allPrintJobs
const mostUsedPrinters = allPrintJobs
.map((job) => job.printer.name)
.reduce((acc, curr) => {
.reduce<Record<string, number>>((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1;
return acc;
}, {});*/
}, {});
const mostUsedPrinter = 0; /*Object.keys(mostUsedPrinters).reduce((a, b) =>
const mostUsedPrinter = Object.keys(mostUsedPrinters).reduce((a, b) =>
mostUsedPrinters[a] > mostUsedPrinters[b] ? a : b,
);*/
);
const printerSuccessRate = (allPrintJobs.filter((job) => job.aborted).length / allPrintJobs.length) * 100;
const mostUsedWeekday = {printer:{name:'-'}}; /*allPrintJobs
const mostUsedWeekday = allPrintJobs
.map((job) => job.startAt.getDay())
.reduce((acc, curr) => {
.reduce<Record<string, number>>((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1;
return acc;
}, {});*/
}, {});
const mostUsedWeekdayIndex = ""; /*Object.keys(mostUsedWeekday).reduce((a, b) =>
const mostUsedWeekdayIndex = Object.keys(mostUsedWeekday).reduce((a, b) =>
mostUsedWeekday[a] > mostUsedWeekday[b] ? a : b,
);*/
);
const mostUsedWeekdayName = new Intl.DateTimeFormat("de-DE", {
weekday: "long",

View File

@@ -20,6 +20,8 @@ export function Countdown(props: CountdownProps) {
return <>...</>;
}
console.log(data);
const days = Math.floor(data.remainingTime / (1000 * 60 * 60 * 24));
const hours = Math.floor((data.remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((data.remainingTime % (1000 * 60 * 60)) / (1000 * 60));

View File

@@ -33,25 +33,23 @@ export function PrinterCard(props: PrinterCardProps) {
"opacity-50 select-none cursor-not-allowed": status === PrinterStatus.OUT_OF_ORDER,
})}
>
<CardHeader>
<div className="flex flex-row items-start justify-between">
<div>
<CardTitle>{printer.name}</CardTitle>
<CardDescription>{printer.description}</CardDescription>
</div>
<Badge
className={cn({
"bg-green-500 hover:bg-green-400": status === PrinterStatus.IDLE,
"bg-red-500 hover:bg-red-500": status === PrinterStatus.OUT_OF_ORDER,
"bg-yellow-500 hover:bg-yellow-400": status === PrinterStatus.RESERVED,
})}
>
{status === PrinterStatus.RESERVED && <Countdown jobId={printer.printJobs[0].id} />}
<If condition={status === PrinterStatus.RESERVED}>
<Else>{translatePrinterStatus(status)}</Else>
</If>
</Badge>
<CardHeader className="flex flex-row justify-between">
<div>
<CardTitle>{printer.name}</CardTitle>
<CardDescription>{printer.description}</CardDescription>
</div>
<Badge
className={cn({
"bg-green-500 hover:bg-green-400": status === PrinterStatus.IDLE,
"bg-red-500 hover:bg-red-500": status === PrinterStatus.OUT_OF_ORDER,
"bg-yellow-500 hover:bg-yellow-400": status === PrinterStatus.RESERVED,
})}
>
{status === PrinterStatus.RESERVED && <Countdown jobId={printer.printJobs[0].id} />}
<If condition={status === PrinterStatus.RESERVED}>
<Else>{translatePrinterStatus(status)}</Else>
</If>
</Badge>
</CardHeader>
<CardContent className="flex justify-end">
<If condition={status === PrinterStatus.IDLE && userIsLoggedIn && !hasRole(user, UserRole.GUEST)}>

View File

View File

View File

View File

View File

View File

View File

View File

@@ -1,370 +0,0 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import {
NameType,
Payload,
ValueType,
} from "recharts/types/component/DefaultTooltipContent"
import { cn } from "@/utils/styles"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File

View File