This file is a merged representation of the entire codebase, combining all repository files into a single document.
Generated by Repomix on: 2024-12-09T06:29:51.427Z

================================================================
File Summary
================================================================

Purpose:
--------
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.

File Format:
------------
The content is organized as follows:
1. This summary section
2. Repository information
3. Repository structure
4. Multiple file entries, each consisting of:
  a. A separator line (================)
  b. The file path (File: path/to/file)
  c. Another separator line
  d. The full contents of the file
  e. A blank line

Usage Guidelines:
-----------------
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.

Notes:
------
- Some files may have been excluded based on .gitignore rules and Repomix's
  configuration.
- Binary files are not included in this packed representation. Please refer to
  the Repository Structure section for a complete list of file paths, including
  binary files.

Additional Info:
----------------

For more information about Repomix, visit: https://github.com/yamadashy/repomix

================================================================
Repository Structure
================================================================
.dockerignore
.env.example
.gitignore
biome.json
components.json
docker/build.sh
docker/caddy/Caddyfile
docker/compose.yml
docker/deploy.sh
docker/images/.gitattributes
docker/save.sh
Dockerfile
drizzle.config.ts
drizzle/0000_overjoyed_strong_guy.sql
drizzle/meta/_journal.json
drizzle/meta/0000_snapshot.json
next.config.mjs
package.json
postcss.config.mjs
public/next.svg
public/vercel.svg
README.md
scripts/generate-data.js
src/app/admin/about/page.tsx
src/app/admin/admin-sidebar.tsx
src/app/admin/charts/printer-error-chart.tsx
src/app/admin/charts/printer-error-rate.tsx
src/app/admin/charts/printer-forecast.tsx
src/app/admin/charts/printer-utilization.tsx
src/app/admin/charts/printer-volume.tsx
src/app/admin/jobs/page.tsx
src/app/admin/layout.tsx
src/app/admin/page.tsx
src/app/admin/printers/columns.tsx
src/app/admin/printers/data-table.tsx
src/app/admin/printers/dialogs/create-printer.tsx
src/app/admin/printers/dialogs/delete-printer.tsx
src/app/admin/printers/dialogs/edit-printer.tsx
src/app/admin/printers/form.tsx
src/app/admin/printers/page.tsx
src/app/admin/settings/download/route.ts
src/app/admin/settings/page.tsx
src/app/admin/users/columns.tsx
src/app/admin/users/data-table.tsx
src/app/admin/users/dialog.tsx
src/app/admin/users/form.tsx
src/app/admin/users/page.tsx
src/app/api/job/[jobId]/remaining-time/route.ts
src/app/api/printers/route.ts
src/app/auth/login/callback/route.ts
src/app/auth/login/route.ts
src/app/globals.css
src/app/job/[jobId]/cancel-form.tsx
src/app/job/[jobId]/edit-comments.tsx
src/app/job/[jobId]/extend-form.tsx
src/app/job/[jobId]/finish-form.tsx
src/app/job/[jobId]/page.tsx
src/app/layout.tsx
src/app/my/jobs/columns.tsx
src/app/my/jobs/data-table.tsx
src/app/my/profile/page.tsx
src/app/not-found.tsx
src/app/page.tsx
src/app/printer/[printerId]/reserve/form.tsx
src/app/printer/[printerId]/reserve/page.tsx
src/components/data-card.tsx
src/components/dynamic-printer-cards.tsx
src/components/header/index.tsx
src/components/header/navigation.tsx
src/components/login-button.tsx
src/components/logout-button.tsx
src/components/personalized-cards.tsx
src/components/printer-availability-badge.tsx
src/components/printer-card/countdown.tsx
src/components/printer-card/index.tsx
src/components/ui/alert-dialog.tsx
src/components/ui/alert.tsx
src/components/ui/avatar.tsx
src/components/ui/badge.tsx
src/components/ui/breadcrumb.tsx
src/components/ui/button.tsx
src/components/ui/card.tsx
src/components/ui/chart.tsx
src/components/ui/dialog.tsx
src/components/ui/dropdown-menu.tsx
src/components/ui/form.tsx
src/components/ui/hover-card.tsx
src/components/ui/input.tsx
src/components/ui/label.tsx
src/components/ui/scroll-area.tsx
src/components/ui/select.tsx
src/components/ui/skeleton.tsx
src/components/ui/sonner.tsx
src/components/ui/table.tsx
src/components/ui/tabs.tsx
src/components/ui/textarea.tsx
src/components/ui/toast.tsx
src/components/ui/toaster.tsx
src/components/ui/use-toast.ts
src/server/actions/authentication/logout.ts
src/server/actions/printers.ts
src/server/actions/printJobs.ts
src/server/actions/timer.ts
src/server/actions/user/delete.ts
src/server/actions/user/update.ts
src/server/actions/users.ts
src/server/auth/index.ts
src/server/auth/oauth.ts
src/server/auth/permissions.ts
src/utils/analytics/error-rate.ts
src/utils/analytics/errors.ts
src/utils/analytics/forecast.ts
src/utils/analytics/utilization.ts
src/utils/analytics/volume.ts
src/utils/drizzle.ts
src/utils/errors.ts
src/utils/fetch.ts
src/utils/guard.ts
src/utils/printers.ts
src/utils/strings.ts
src/utils/styles.ts
tailwind.config.ts
tsconfig.json

================================================================
Repository Files
================================================================

================
File: .dockerignore
================
# Build and utility assets
docker/
scripts/

# Ignore node_modules as they will be installed in the container
node_modules

# Ignore build artifacts
.next

# Ignore runtime data
db/

# Ignore local configuration files
.env
.env.example

# Ignore version control files
.git
.gitignore

# Ignore IDE/editor specific files
*.log
*.tmp
*.DS_Store
.vscode/
.idea/

================
File: .env.example
================
# OAuth Configuration
OAUTH_CLIENT_ID=client_id
OAUTH_CLIENT_SECRET=client_secret

================
File: .gitignore
================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# db folder
db/

# Env file
.env


# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

================
File: biome.json
================
{
  "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "formatter": {
    "enabled": true,
    "lineWidth": 120
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedImports": "error"
      }
    }
  }
}

================
File: components.json
================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/utils/styles"
  }
}

================
File: docker/build.sh
================
#!/bin/bash

# Define image name
MYP_RP_IMAGE_NAME="myp-rp"

# Function to build Docker image
build_image() {
    local image_name=$1
    local dockerfile=$2
    local platform=$3

    echo "Building $image_name Docker image for $platform..."

    docker buildx build --platform $platform -t ${image_name}:latest -f $dockerfile --load .
    if [ $? -eq 0 ]; then
        echo "$image_name Docker image built successfully"
    else
        echo "Error occurred while building $image_name Docker image"
        exit 1
    fi
}

# Create and use a builder instance (if not already created)
BUILDER_NAME="myp-rp-arm64-builder"
docker buildx create --name $BUILDER_NAME --use || docker buildx use $BUILDER_NAME

# Build myp-rp image
build_image "$MYP_RP_IMAGE_NAME" "$PWD/Dockerfile" "linux/arm64"

# Remove the builder instance
docker buildx rm $BUILDER_NAME

================
File: docker/caddy/Caddyfile
================
{
    debug
}

m040tbaraspi001.de040.corpintra.net, m040tbaraspi001.de040.corpinter.net {
    reverse_proxy myp-rp:3000
    tls internal
}

================
File: docker/compose.yml
================
services:
  caddy:
    image: caddy:2.8
    container_name: caddy
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./caddy/data:/data
      - ./caddy/config:/config
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
  myp-rp:
    image: myp-rp:latest
    container_name: myp-rp
    env_file: "/srv/myp-env/github.env"
    volumes:
      - /srv/MYP-DB:/usr/src/app/db
    restart: unless-stopped

================
File: docker/deploy.sh
================
#!/bin/bash

# Directory containing the Docker images
IMAGE_DIR="docker/images"

# Load all Docker images from the tar.xz files in the IMAGE_DIR
echo "Loading Docker images from $IMAGE_DIR..."

for image_file in "$IMAGE_DIR"/*.tar.xz; do
    if [ -f "$image_file" ]; then
        echo "Loading Docker image from $image_file..."
        docker load -i "$image_file"

        # Check if the image loading was successful
        if [ $? -ne 0 ]; then
            echo "Error occurred while loading Docker image from $image_file"
            exit 1
        fi
    else
        echo "No Docker image tar.xz files found in $IMAGE_DIR."
    fi
done

# Execute docker compose
echo "Running docker compose..."
docker compose -f "docker/compose.yml" up -d

# Check if the operation was successful
if [ $? -eq 0 ]; then
    echo "Docker compose executed successfully"
else
    echo "Error occurred while executing docker compose"
    exit 1
fi

echo "Deployment completed successfully"

================
File: docker/images/.gitattributes
================
caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text
myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text

================
File: docker/save.sh
================
#!/bin/bash

# Get image name as argument
IMAGE_NAME=$1
PLATFORM="linux/arm64"

# Define paths
IMAGE_DIR="docker/images"
IMAGE_FILE="${IMAGE_DIR}/${IMAGE_NAME//[:\/]/_}.tar"
COMPRESSED_FILE="${IMAGE_FILE}.xz"

# Function to pull the image
pull_image() {
    local image=$1
    if [[ $image == arm64v8/* ]]; then
        echo "Pulling image $image without platform specification..."
        docker pull $image
    else
        echo "Pulling image $image for platform $PLATFORM..."
        docker pull --platform $PLATFORM $image
    fi
    return $?
}

# Pull the image if it is not available locally
if ! docker image inspect ${IMAGE_NAME} &>/dev/null; then
    if pull_image ${IMAGE_NAME}; then
        echo "Image $IMAGE_NAME pulled successfully."
    else
        echo "Error occurred while pulling $IMAGE_NAME for platform $PLATFORM"
        echo "Trying to pull $IMAGE_NAME without platform specification..."

        # Attempt to pull again without platform
        if pull_image ${IMAGE_NAME}; then
            echo "Image $IMAGE_NAME pulled successfully without platform."
        else
            echo "Error occurred while pulling $IMAGE_NAME without platform."
            echo "Trying to pull arm64v8/${IMAGE_NAME} instead..."

            # Construct new image name
            NEW_IMAGE_NAME="arm64v8/${IMAGE_NAME}"
            if pull_image ${NEW_IMAGE_NAME}; then
                echo "Image $NEW_IMAGE_NAME pulled successfully."
                IMAGE_NAME=${NEW_IMAGE_NAME}  # Update IMAGE_NAME to use the new one
            else
                echo "Error occurred while pulling $NEW_IMAGE_NAME"
                exit 1
            fi
        fi
    fi
else
    echo "Image $IMAGE_NAME found locally. Skipping pull."
fi

# Save the Docker image
echo "Saving $IMAGE_NAME Docker image..."
docker save ${IMAGE_NAME} > $IMAGE_FILE

# Compress the Docker image (overwriting if file exists)
echo "Compressing $IMAGE_FILE..."
xz -z --force $IMAGE_FILE

if [ $? -eq 0 ]; then
    echo "$IMAGE_NAME Docker image saved and compressed successfully as $COMPRESSED_FILE"
else
    echo "Error occurred while compressing $IMAGE_NAME Docker image"
    exit 1
fi

================
File: Dockerfile
================
FROM node:20-bookworm-slim

# Create application directory
RUN mkdir -p /usr/src/app

# Set environment variables
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1

WORKDIR /usr/src/app

# Copy package.json and pnpm-lock.yaml
COPY package.json /usr/src/app
COPY pnpm-lock.yaml /usr/src/app

# Install pnpm
RUN corepack enable pnpm

# Install dependencies
RUN pnpm install

# Copy the rest of the application code
COPY . /usr/src/app

# Initialize Database, if it not already exists
RUN pnpm run db

# Build the application
RUN pnpm run build

EXPOSE 3000

# Start the application
CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"]

================
File: drizzle.config.ts
================
import { defineConfig } from "drizzle-kit";

//@ts-ignore - better-sqlite driver throws an error even though its an valid value
export default defineConfig({
	dialect: "sqlite",
	schema: "./src/server/db/schema.ts",
	out: "./drizzle",
	driver: "libsql",
	dbCredentials: {
		url: "file:./db/sqlite.db",
	},
});

================
File: drizzle/0000_overjoyed_strong_guy.sql
================
CREATE TABLE `printJob` (
	`id` text PRIMARY KEY NOT NULL,
	`printerId` text NOT NULL,
	`userId` text NOT NULL,
	`startAt` integer NOT NULL,
	`durationInMinutes` integer NOT NULL,
	`comments` text,
	`aborted` integer DEFAULT false NOT NULL,
	`abortReason` text,
	FOREIGN KEY (`printerId`) REFERENCES `printer`(`id`) ON UPDATE no action ON DELETE cascade,
	FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `printer` (
	`id` text PRIMARY KEY NOT NULL,
	`name` text NOT NULL,
	`description` text NOT NULL,
	`status` integer DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE `session` (
	`id` text PRIMARY KEY NOT NULL,
	`user_id` text NOT NULL,
	`expires_at` integer NOT NULL,
	FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `user` (
	`id` text PRIMARY KEY NOT NULL,
	`github_id` integer NOT NULL,
	`name` text,
	`displayName` text,
	`email` text NOT NULL,
	`role` text DEFAULT 'guest'
);

================
File: drizzle/meta/_journal.json
================
{
  "version": "6",
  "dialect": "sqlite",
  "entries": [
    {
      "idx": 0,
      "version": "6",
      "when": 1715416514336,
      "tag": "0000_overjoyed_strong_guy",
      "breakpoints": true
    }
  ]
}

================
File: drizzle/meta/0000_snapshot.json
================
{
  "version": "6",
  "dialect": "sqlite",
  "id": "791dc197-5254-4432-bd9f-1368d1a5aa6a",
  "prevId": "00000000-0000-0000-0000-000000000000",
  "tables": {
    "printJob": {
      "name": "printJob",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "printerId": {
          "name": "printerId",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "userId": {
          "name": "userId",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "startAt": {
          "name": "startAt",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "durationInMinutes": {
          "name": "durationInMinutes",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "comments": {
          "name": "comments",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "aborted": {
          "name": "aborted",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false,
          "default": false
        },
        "abortReason": {
          "name": "abortReason",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        }
      },
      "indexes": {},
      "foreignKeys": {
        "printJob_printerId_printer_id_fk": {
          "name": "printJob_printerId_printer_id_fk",
          "tableFrom": "printJob",
          "tableTo": "printer",
          "columnsFrom": [
            "printerId"
          ],
          "columnsTo": [
            "id"
          ],
          "onDelete": "cascade",
          "onUpdate": "no action"
        },
        "printJob_userId_user_id_fk": {
          "name": "printJob_userId_user_id_fk",
          "tableFrom": "printJob",
          "tableTo": "user",
          "columnsFrom": [
            "userId"
          ],
          "columnsTo": [
            "id"
          ],
          "onDelete": "cascade",
          "onUpdate": "no action"
        }
      },
      "compositePrimaryKeys": {},
      "uniqueConstraints": {}
    },
    "printer": {
      "name": "printer",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "name": {
          "name": "name",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "description": {
          "name": "description",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "status": {
          "name": "status",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false,
          "default": 0
        }
      },
      "indexes": {},
      "foreignKeys": {},
      "compositePrimaryKeys": {},
      "uniqueConstraints": {}
    },
    "session": {
      "name": "session",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "user_id": {
          "name": "user_id",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "expires_at": {
          "name": "expires_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        }
      },
      "indexes": {},
      "foreignKeys": {
        "session_user_id_user_id_fk": {
          "name": "session_user_id_user_id_fk",
          "tableFrom": "session",
          "tableTo": "user",
          "columnsFrom": [
            "user_id"
          ],
          "columnsTo": [
            "id"
          ],
          "onDelete": "cascade",
          "onUpdate": "no action"
        }
      },
      "compositePrimaryKeys": {},
      "uniqueConstraints": {}
    },
    "user": {
      "name": "user",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "github_id": {
          "name": "github_id",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "name": {
          "name": "name",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "displayName": {
          "name": "displayName",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "email": {
          "name": "email",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "role": {
          "name": "role",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false,
          "default": "'guest'"
        }
      },
      "indexes": {},
      "foreignKeys": {},
      "compositePrimaryKeys": {},
      "uniqueConstraints": {}
    }
  },
  "enums": {},
  "_meta": {
    "schemas": {},
    "tables": {},
    "columns": {}
  }
}

================
File: next.config.mjs
================
/** @type {import('next').NextConfig} */
const nextConfig = {
	async headers() {
		return [
			{
				source: "/:path*",
				headers: [
					{
						key: "Access-Control-Allow-Origin",
						value: "m040tbaraspi001.de040.corpintra.net",
					},
					{
						key: "Access-Control-Allow-Methods",
						value: "GET, POST, PUT, DELETE, OPTIONS",
					},
					{
						key: "Access-Control-Allow-Headers",
						value: "Content-Type, Authorization",
					},
				],
			},
		];
	},
};

export default nextConfig;

================
File: package.json
================
{
  "name": "myp-rp",
  "version": "1.0.0",
  "private": true,
  "packageManager": "pnpm@9.12.1",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "db:create-default": "mkdir -p db/",
    "db:generate-sqlite": "pnpm drizzle-kit generate",
    "db:clean": "rm -rf db/ drizzle/",
    "db:migrate": "pnpm drizzle-kit migrate",
    "db": "pnpm db:create-default && pnpm db:generate-sqlite && pnpm db:migrate",
    "db:reset": "pnpm db:clean && pnpm db"
  },
  "dependencies": {
    "@faker-js/faker": "^9.2.0",
    "@headlessui/react": "^2.1.10",
    "@headlessui/tailwindcss": "^0.2.1",
    "@hookform/resolvers": "^3.9.0",
    "@libsql/client": "^0.14.0",
    "@lucia-auth/adapter-drizzle": "^1.1.0",
    "@radix-ui/react-alert-dialog": "^1.1.2",
    "@radix-ui/react-avatar": "^1.1.1",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-dropdown-menu": "^2.1.2",
    "@radix-ui/react-hover-card": "^1.1.2",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-scroll-area": "^1.2.0",
    "@radix-ui/react-select": "^2.1.2",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.1",
    "@radix-ui/react-toast": "^1.2.2",
    "@remixicon/react": "^4.3.0",
    "@tanstack/react-table": "^8.20.5",
    "@tremor/react": "^3.18.3",
    "arctic": "^1.9.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "date-fns": "^4.1.0",
    "drizzle-orm": "^0.30.10",
    "lodash": "^4.17.21",
    "lucia": "^3.2.1",
    "lucide-react": "^0.378.0",
    "luxon": "^3.5.0",
    "next": "14.2.3",
    "next-themes": "^0.3.0",
    "oslo": "^1.2.1",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-hook-form": "^7.53.0",
    "react-if": "^4.1.5",
    "react-timer-hook": "^3.0.7",
    "recharts": "^2.13.3",
    "regression": "^2.0.1",
    "sonner": "^1.5.0",
    "sqlite": "^5.1.1",
    "sqlite3": "^5.1.7",
    "swr": "^2.2.5",
    "tailwind-merge": "^2.5.3",
    "tailwindcss-animate": "^1.0.7",
    "use-debounce": "^10.0.3",
    "uuid": "^11.0.2",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@biomejs/biome": "^1.9.3",
    "@tailwindcss/forms": "^0.5.9",
    "@types/lodash": "^4.17.13",
    "@types/luxon": "^3.4.2",
    "@types/node": "^20.16.11",
    "@types/react": "^18.3.11",
    "@types/react-dom": "^18.3.1",
    "drizzle-kit": "^0.21.4",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.13",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.3"
  }
}

================
File: postcss.config.mjs
================
/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    tailwindcss: {},
  },
};

export default config;

================
File: public/next.svg
================
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

================
File: public/vercel.svg
================
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

================
File: README.md
================
# MYP - Manage Your Printer

MYP (Manage Your Printer) ist eine Webanwendung zur Reservierung von 3D-Druckern.
Sie wurde im Rahmen des Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt.

## Deployment

### Voraussetzungen

- Netzwerk auf Raspberry Pi ist eingerichtet
- Docker ist installiert

### Schritte

1. Docker-Container bauen (docker/build.sh)
2. Docker-Container speichern (docker/save.sh caddy:2.8 myp-rp:latest)
3. Docker-Container auf Raspberry Pi bereitstellen (docker/deploy.sh)

## Entwicklerinformationen

### Raspberry Pi Einstellungen

Auf dem Raspberry Pi wurde Raspbian Lite installiert.
Unter /srv/* sind die Projektdateien zu finden.

### Anmeldedaten

```
Benutzer: myp
Passwort: (persönlich bekannt)
```

================
File: scripts/generate-data.js
================
const sqlite3 = require("sqlite3");
const faker = require("@faker-js/faker").faker;
const { random, sample, sampleSize, sum } = require("lodash");
const { DateTime } = require("luxon");
const { open } = require("sqlite");
const { v4: uuidv4 } = require("uuid");

const dbPath = "./db/sqlite.db";

// Configuration for test data generation
let startDate = DateTime.fromISO("2024-10-08");
let endDate = DateTime.fromISO("2024-11-08");
let numberOfPrinters = 5;

// Use weekday names for better readability and ease of setting trends
let avgPrintTimesPerDay = {
	Monday: 4,
	Tuesday: 2,
	Wednesday: 5,
	Thursday: 2,
	Friday: 3,
	Saturday: 0,
	Sunday: 0,
}; // Average number of prints for each weekday

let avgPrintDurationPerDay = {
	Monday: 240, // Total average duration in minutes for Monday
	Tuesday: 30,
	Wednesday: 45,
	Thursday: 40,
	Friday: 120,
	Saturday: 0,
	Sunday: 0,
}; // Average total duration of prints for each weekday

let printerUsage = {
	"Drucker 1": 0.5,
	"Drucker 2": 0.7,
	"Drucker 3": 0.6,
	"Drucker 4": 0.3,
	"Drucker 5": 0.4,
}; // Usage percentages for each printer

// **New Configurations for Error Rates**
let generalErrorRate = 0.05; // 5% chance any print job may fail
let printerErrorRates = {
	"Drucker 1": 0.02, // 2% error rate for Printer 1
	"Drucker 2": 0.03,
	"Drucker 3": 0.01,
	"Drucker 4": 0.05,
	"Drucker 5": 0.04,
}; // Error rates for each printer

const holidays = []; // Example holidays
const existingJobs = [];

const initDB = async () => {
	console.log("Initializing database connection...");
	return open({
		filename: dbPath,
		driver: sqlite3.Database,
	});
};

const createUser = (isPowerUser = false) => {
	const name = [faker.person.firstName(), faker.person.lastName()];

	const user = {
		id: uuidv4(),
		github_id: faker.number.int(),
		username: `${name[0].slice(0, 2)}${name[1].slice(0, 6)}`.toUpperCase(),
		displayName: `${name[0]} ${name[1]}`.toUpperCase(),
		email: `${name[0]}.${name[1]}@example.com`,
		role: sample(["user", "admin"]),
		isPowerUser,
	};
	console.log("Created user:", user);
	return user;
};

const createPrinter = (index) => {
	const printer = {
		id: uuidv4(),
		name: `Drucker ${index}`,
		description: faker.lorem.sentence(),
		status: random(0, 2),
	};
	console.log("Created printer:", printer);
	return printer;
};

const isPrinterAvailable = (printer, startAt, duration) => {
	const endAt = startAt + duration * 60 * 1000; // Convert minutes to milliseconds
	return !existingJobs.some((job) => {
		const jobStart = job.startAt;
		const jobEnd = job.startAt + job.durationInMinutes * 60 * 1000;
		return (
			printer.id === job.printerId &&
			((startAt >= jobStart && startAt < jobEnd) ||
				(endAt > jobStart && endAt <= jobEnd) ||
				(startAt <= jobStart && endAt >= jobEnd))
		);
	});
};

const createPrintJob = (users, printers, startAt, duration) => {
	const user = sample(users);
	let printer;

	// Weighted selection based on printer usage
	const printerNames = Object.keys(printerUsage);
	const weightedPrinters = printers.filter((p) => printerNames.includes(p.name));

	// Create a weighted array of printers based on usage percentages
	const printerWeights = weightedPrinters.map((p) => ({
		printer: p,
		weight: printerUsage[p.name],
	}));

	const totalWeight = sum(printerWeights.map((pw) => pw.weight));
	const randomWeight = Math.random() * totalWeight;
	let accumulatedWeight = 0;
	for (const pw of printerWeights) {
		accumulatedWeight += pw.weight;
		if (randomWeight <= accumulatedWeight) {
			printer = pw.printer;
			break;
		}
	}

	if (!printer) {
		printer = sample(printers);
	}

	if (!isPrinterAvailable(printer, startAt, duration)) {
		console.log("Printer not available, skipping job creation.");
		return null;
	}

	// **Determine if the job should be aborted based on error rates**
	let aborted = false;
	let abortReason = null;

	// Calculate the combined error rate
	const printerErrorRate = printerErrorRates[printer.name] || 0;
	const combinedErrorRate = 1 - (1 - generalErrorRate) * (1 - printerErrorRate);

	if (Math.random() < combinedErrorRate) {
		aborted = true;
		const errorMessages = [
			"Unbekannt",
			"Keine Ahnung",
			"Falsch gebucht",
			"Filament gelöst",
			"Druckabbruch",
			"Düsenverstopfung",
			"Schichthaftung fehlgeschlagen",
			"Materialmangel",
			"Dateifehler",
			"Temperaturproblem",
			"Mechanischer Fehler",
			"Softwarefehler",
			"Kalibrierungsfehler",
			"Überhitzung",
		];
		abortReason = sample(errorMessages); // Generate a random abort reason
	}

	const printJob = {
		id: uuidv4(),
		printerId: printer.id,
		userId: user.id,
		startAt,
		durationInMinutes: duration,
		comments: faker.lorem.sentence(),
		aborted,
		abortReason,
	};
	console.log("Created print job:", printJob);
	return printJob;
};

const generatePrintJobsForDay = async (users, printers, dayDate, totalJobsForDay, totalDurationForDay, db, dryRun) => {
	console.log(`Generating print jobs for ${dayDate.toISODate()}...`);

	// Generate random durations that sum up approximately to totalDurationForDay
	const durations = [];
	let remainingDuration = totalDurationForDay;
	for (let i = 0; i < totalJobsForDay; i++) {
		const avgJobDuration = remainingDuration / (totalJobsForDay - i);
		const jobDuration = Math.max(
			Math.round(random(avgJobDuration * 0.8, avgJobDuration * 1.2)),
			5, // Minimum duration of 5 minutes
		);
		durations.push(jobDuration);
		remainingDuration -= jobDuration;
	}

	// Shuffle durations to randomize job lengths
	const shuffledDurations = sampleSize(durations, durations.length);

	for (let i = 0; i < totalJobsForDay; i++) {
		const duration = shuffledDurations[i];

		// Random start time between 8 AM and 6 PM, adjusted to avoid overlapping durations
		const possibleStartHours = Array.from({ length: 10 }, (_, idx) => idx + 8); // 8 AM to 6 PM
		let startAt;
		let attempts = 0;
		do {
			const hour = sample(possibleStartHours);
			const minute = random(0, 59);
			startAt = dayDate.set({ hour, minute, second: 0, millisecond: 0 }).toMillis();
			attempts++;
			if (attempts > 10) {
				console.log("Unable to find available time slot, skipping job.");
				break;
			}
		} while (!isPrinterAvailable(sample(printers), startAt, duration));

		if (attempts > 10) continue;

		const printJob = createPrintJob(users, printers, startAt, duration);
		if (printJob) {
			if (!dryRun) {
				await db.run(
					`INSERT INTO printJob (id, printerId, userId, startAt, durationInMinutes, comments, aborted, abortReason)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
					[
						printJob.id,
						printJob.printerId,
						printJob.userId,
						printJob.startAt,
						printJob.durationInMinutes,
						printJob.comments,
						printJob.aborted ? 1 : 0,
						printJob.abortReason,
					],
				);
			}
			existingJobs.push(printJob);
			console.log("Inserted print job into database:", printJob.id);
		}
	}
};

const generateTestData = async (dryRun = false) => {
	console.log("Starting test data generation...");
	const db = await initDB();

	// Generate users and printers
	const users = [
		...Array.from({ length: 7 }, () => createUser(false)),
		...Array.from({ length: 3 }, () => createUser(true)),
	];
	const printers = Array.from({ length: numberOfPrinters }, (_, index) => createPrinter(index + 1));

	if (!dryRun) {
		// Insert users into the database
		for (const user of users) {
			await db.run(
				`INSERT INTO user (id, github_id, name, displayName, email, role)
         VALUES (?, ?, ?, ?, ?, ?)`,
				[user.id, user.github_id, user.username, user.displayName, user.email, user.role],
			);
			console.log("Inserted user into database:", user.id);
		}

		// Insert printers into the database
		for (const printer of printers) {
			await db.run(
				`INSERT INTO printer (id, name, description, status)
         VALUES (?, ?, ?, ?)`,
				[printer.id, printer.name, printer.description, printer.status],
			);
			console.log("Inserted printer into database:", printer.id);
		}
	}

	// Generate print jobs for each day within the specified date range
	let currentDay = startDate;
	while (currentDay <= endDate) {
		const weekdayName = currentDay.toFormat("EEEE"); // Get weekday name (e.g., 'Monday')
		if (holidays.includes(currentDay.toISODate()) || avgPrintTimesPerDay[weekdayName] === 0) {
			console.log(`Skipping holiday or no jobs scheduled: ${currentDay.toISODate()}`);
			currentDay = currentDay.plus({ days: 1 });
			continue;
		}

		const totalJobsForDay = avgPrintTimesPerDay[weekdayName];
		const totalDurationForDay = avgPrintDurationPerDay[weekdayName];

		await generatePrintJobsForDay(users, printers, currentDay, totalJobsForDay, totalDurationForDay, db, dryRun);
		currentDay = currentDay.plus({ days: 1 });
	}

	if (!dryRun) {
		await db.close();
		console.log("Database connection closed. Test data generation complete.");
	} else {
		console.log("Dry run complete. No data was written to the database.");
	}
};

const setConfigurations = (config) => {
	if (config.startDate) startDate = DateTime.fromISO(config.startDate);
	if (config.endDate) endDate = DateTime.fromISO(config.endDate);
	if (config.numberOfPrinters) numberOfPrinters = config.numberOfPrinters;
	if (config.avgPrintTimesPerDay) avgPrintTimesPerDay = config.avgPrintTimesPerDay;
	if (config.avgPrintDurationPerDay) avgPrintDurationPerDay = config.avgPrintDurationPerDay;
	if (config.printerUsage) printerUsage = config.printerUsage;
	if (config.generalErrorRate !== undefined) generalErrorRate = config.generalErrorRate;
	if (config.printerErrorRates) printerErrorRates = config.printerErrorRates;
};

// Example usage
setConfigurations({
	startDate: "2024-10-08",
	endDate: "2024-11-08",
	numberOfPrinters: 6,
	avgPrintTimesPerDay: {
		Monday: 4, // High usage
		Tuesday: 2, // Low usage
		Wednesday: 3, // Low usage
		Thursday: 2, // Low usage
		Friday: 8, // High usage
		Saturday: 0,
		Sunday: 0,
	},
	avgPrintDurationPerDay: {
		Monday: 300, // High total duration
		Tuesday: 60, // Low total duration
		Wednesday: 90,
		Thursday: 60,
		Friday: 240,
		Saturday: 0,
		Sunday: 0,
	},
	printerUsage: {
		"Drucker 1": 2.3,
		"Drucker 2": 1.7,
		"Drucker 3": 0.1,
		"Drucker 4": 1.5,
		"Drucker 5": 2.4,
		"Drucker 6": 0.3,
		"Drucker 7": 0.9,
		"Drucker 8": 0.1,
	},
	generalErrorRate: 0.05, // 5% general error rate
	printerErrorRates: {
		"Drucker 1": 0.02,
		"Drucker 2": 0.03,
		"Drucker 3": 0.1,
		"Drucker 4": 0.05,
		"Drucker 5": 0.04,
		"Drucker 6": 0.02,
		"Drucker 7": 0.01,
		"PrinteDrucker 8": 0.03,
	},
});

generateTestData(process.argv.includes("--dry-run"))
	.then(() => {
		console.log("Test data generation script finished.");
	})
	.catch((err) => {
		console.error("Error generating test data:", err);
	});

================
File: src/app/admin/about/page.tsx
================
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { Metadata } from "next";

export const metadata: Metadata = {
	title: "Über MYP",
};

export default async function AdminPage() {
	return (
		<Card>
			<CardHeader>
				<CardTitle>Über MYP</CardTitle>
				<CardDescription>
					<i className="italic">MYP &mdash; Manage Your Printer</i>
				</CardDescription>
			</CardHeader>
			<CardContent className="gap-y-2 flex flex-col">
				<p className="max-w-[80ch]">
					<strong>MYP</strong> ist eine Webanwendung zur Reservierung von 3D-Druckern. Sie wurde im Rahmen des
					Abschlussprojektes der Fachinformatiker Ausbildung für Daten- und Prozessanalyse für die Technische
					Berufsausbildung des Mercedes-Benz Werkes Berlin-Marienfelde entwickelt.
				</p>
				<p>
					&copy; 2024{" "}
					<a href="https://linkedin.com/in/torben-haack" target="_blank" rel="noreferrer">
						Torben Haack
					</a>
				</p>
			</CardContent>
		</Card>
	);
}

================
File: src/app/admin/admin-sidebar.tsx
================
"use client";

import { cn } from "@/utils/styles";
import { FileIcon, HeartIcon, LayoutDashboardIcon, PrinterIcon, UsersIcon, WrenchIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";

interface AdminSite {
	name: string;
	path: string;
	icon: React.ReactNode;
}

export function AdminSidebar() {
	const pathname = usePathname();
	const adminSites: AdminSite[] = [
		{
			name: "Dashboard",
			path: "/admin",
			icon: <LayoutDashboardIcon className="w-4 h-4" />,
		},
		{
			name: "Benutzer",
			path: "/admin/users",
			icon: <UsersIcon className="w-4 h-4" />,
		},
		{
			name: "Drucker",
			path: "/admin/printers",
			icon: <PrinterIcon className="w-4 h-4" />,
		},
		{
			name: "Druckaufträge",
			path: "/admin/jobs",
			icon: <FileIcon className="w-4 h-4" />,
		},
		{
			name: "Einstellungen",
			path: "/admin/settings",
			icon: <WrenchIcon className="w-4 h-4" />,
		},
		{
			name: "Über MYP",
			path: "/admin/about",
			icon: <HeartIcon className="w-4 h-4" />,
		},
	];

	return (
		<ul className="w-full">
			{adminSites.map((site) => (
				<li key={site.path}>
					<Link
						href={site.path}
						className={cn("flex items-center gap-2 p-2 rounded hover:bg-muted", {
							"font-semibold": pathname === site.path,
						})}
					>
						{site.icon}
						<span>{site.name}</span>
					</Link>
				</li>
			))}
		</ul>
	);
}

================
File: src/app/admin/charts/printer-error-chart.tsx
================
"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>
	);
}

================
File: src/app/admin/charts/printer-error-rate.tsx
================
"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>
	);
}

================
File: src/app/admin/charts/printer-forecast.tsx
================
"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(", ");
}

================
File: src/app/admin/charts/printer-utilization.tsx
================
"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>
	);
}

================
File: src/app/admin/charts/printer-volume.tsx
================
"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>
	);
}

================
File: src/app/admin/jobs/page.tsx
================
import { columns } from "@/app/my/jobs/columns";
import { JobsTable } from "@/app/my/jobs/data-table";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { db } from "@/server/db";
import { printJobs } from "@/server/db/schema";
import { desc } from "drizzle-orm";
import type { Metadata } from "next";

export const metadata: Metadata = {
	title: "Alle Druckaufträge",
};

export default async function AdminJobsPage() {
	const allJobs = await db.query.printJobs.findMany({
		orderBy: [desc(printJobs.startAt)],
		with: {
			user: true,
			printer: true,
		},
	});

	return (
		<Card>
			<CardHeader className="flex flex-row justify-between items-center">
				<div>
					<CardTitle>Druckaufträge</CardTitle>
					<CardDescription>Alle Druckaufträge</CardDescription>
				</div>
			</CardHeader>
			<CardContent>
				<JobsTable columns={columns} data={allJobs} />
			</CardContent>
		</Card>
	);
}

================
File: src/app/admin/layout.tsx
================
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 { 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)) {
		redirect("/");
	}

	return (
		<main className="flex flex-1 flex-col gap-4">
			<div className="mx-auto grid w-full gap-2">
				<h1 className="text-3xl font-semibold">Admin</h1>
			</div>
			<div className="mx-auto grid w-full items-start gap-4 md:gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]">
				<nav className="grid gap-4 text-sm">
					<AdminSidebar />
				</nav>
				<div>{children}</div>
			</div>
		</main>
	);
}

================
File: src/app/admin/page.tsx
================
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";
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 = {
	title: "Admin Dashboard",
};

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),
		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) => {
		if (job.aborted) return false;

		const endAt = job.startAt.getTime() + job.durationInMinutes * 1000 * 60;

		return endAt > currentDate.getTime();
	});
	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>
				</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>
					</div>
				</TabsContent>
			</Tabs>
		</>
	);
}

================
File: src/app/admin/printers/columns.tsx
================
"use client";
import type { printers } from "@/server/db/schema";
import type { ColumnDef } from "@tanstack/react-table";
import type { InferSelectModel } from "drizzle-orm";
import { ArrowUpDown, MoreHorizontal, PencilIcon } from "lucide-react";

import { EditPrinterDialogContent, EditPrinterDialogTrigger } from "@/app/admin/printers/dialogs/edit-printer";
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { type PrinterStatus, translatePrinterStatus } from "@/utils/printers";
import { useState } from "react";

// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.

export const columns: ColumnDef<InferSelectModel<typeof printers>>[] = [
	{
		accessorKey: "id",
		header: ({ column }) => {
			return (
				<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}>
					ID
					<ArrowUpDown className="ml-2 h-4 w-4" />
				</Button>
			);
		},
	},
	{
		accessorKey: "name",
		header: "Name",
	},
	{
		accessorKey: "description",
		header: "Beschreibung",
	},
	{
		accessorKey: "status",
		header: "Status",
		cell: ({ row }) => {
			const status = row.getValue("status");
			const translated = translatePrinterStatus(status as PrinterStatus);

			return translated;
		},
	},
	{
		id: "actions",
		cell: ({ row }) => {
			const printer = row.original;
			const [open, setOpen] = useState(false);

			return (
				<Dialog open={open} onOpenChange={setOpen}>
					<DropdownMenu>
						<DropdownMenuTrigger asChild>
							<Button variant="ghost" className="h-8 w-8 p-0">
								<span className="sr-only">Menu öffnen</span>
								<MoreHorizontal className="h-4 w-4" />
							</Button>
						</DropdownMenuTrigger>
						<DropdownMenuContent align="end">
							<DropdownMenuLabel>Aktionen</DropdownMenuLabel>
							<DropdownMenuItem asChild>ABC</DropdownMenuItem>
							<DropdownMenuItem>
								<EditPrinterDialogTrigger>
									<div className="flex items-center gap-2">
										<PencilIcon className="w-4 h-4" />
										<span>Bearbeiten</span>
									</div>
								</EditPrinterDialogTrigger>
							</DropdownMenuItem>
						</DropdownMenuContent>
					</DropdownMenu>
					<EditPrinterDialogContent setOpen={setOpen} printer={printer} />
				</Dialog>
			);
		},
	},
];

================
File: src/app/admin/printers/data-table.tsx
================
"use client";

import {
	type ColumnDef,
	type ColumnFiltersState,
	type SortingState,
	type VisibilityState,
	flexRender,
	getCoreRowModel,
	getFilteredRowModel,
	getPaginationRowModel,
	getSortedRowModel,
	useReactTable,
} from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
import {
	DropdownMenu,
	DropdownMenuCheckboxItem,
	DropdownMenuContent,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { SlidersHorizontalIcon } from "lucide-react";
import { useState } from "react";

interface DataTableProps<TData, TValue> {
	columns: ColumnDef<TData, TValue>[];
	data: TData[];
}

export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
	const [sorting, setSorting] = useState<SortingState>([]);
	const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
	const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});

	const table = useReactTable({
		data,
		columns,
		getCoreRowModel: getCoreRowModel(),
		getPaginationRowModel: getPaginationRowModel(),
		onSortingChange: setSorting,
		getSortedRowModel: getSortedRowModel(),
		onColumnFiltersChange: setColumnFilters,
		getFilteredRowModel: getFilteredRowModel(),
		onColumnVisibilityChange: setColumnVisibility,
		state: {
			sorting,
			columnFilters,
			columnVisibility,
		},
	});

	return (
		<div>
			<div className="flex items-center py-4">
				<Input
					placeholder="Name filtern..."
					value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
					onChange={(event) => table.getColumn("name")?.setFilterValue(event.target.value)}
					className="max-w-sm"
				/>
				<DropdownMenu>
					<DropdownMenuTrigger asChild>
						<Button variant="outline" className="ml-auto flex items-center gap-2">
							<SlidersHorizontalIcon className="h-4 w-4" />
							<span>Spalten</span>
						</Button>
					</DropdownMenuTrigger>
					<DropdownMenuContent align="end">
						{table
							.getAllColumns()
							.filter((column) => column.getCanHide())
							.map((column) => {
								return (
									<DropdownMenuCheckboxItem
										key={column.id}
										className="capitalize"
										checked={column.getIsVisible()}
										onCheckedChange={(value) => column.toggleVisibility(!!value)}
									>
										{column.id}
									</DropdownMenuCheckboxItem>
								);
							})}
					</DropdownMenuContent>
				</DropdownMenu>
			</div>

			<div className="rounded-md border">
				<Table>
					<TableHeader>
						{table.getHeaderGroups().map((headerGroup) => (
							<TableRow key={headerGroup.id}>
								{headerGroup.headers.map((header) => {
									return (
										<TableHead key={header.id}>
											{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
										</TableHead>
									);
								})}
							</TableRow>
						))}
					</TableHeader>
					<TableBody>
						{table.getRowModel().rows?.length ? (
							table.getRowModel().rows.map((row) => (
								<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
									{row.getVisibleCells().map((cell) => (
										<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
									))}
								</TableRow>
							))
						) : (
							<TableRow>
								<TableCell colSpan={columns.length} className="h-24 text-center">
									Keine Ergebnisse gefunden.
								</TableCell>
							</TableRow>
						)}
					</TableBody>
				</Table>
			</div>
			<div className="flex items-center justify-end space-x-2 py-4">
				<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
					Zurück
				</Button>
				<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
					Nächste Seite
				</Button>
			</div>
		</div>
	);
}

================
File: src/app/admin/printers/dialogs/create-printer.tsx
================
"use client";

import { PrinterForm } from "@/app/admin/printers/form";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { useState } from "react";

interface CreatePrinterDialogProps {
	children: React.ReactNode;
}

export function CreatePrinterDialog(props: CreatePrinterDialogProps) {
	const { children } = props;
	const [open, setOpen] = useState(false);

	return (
		<Dialog open={open} onOpenChange={setOpen}>
			<DialogTrigger asChild>{children}</DialogTrigger>
			<DialogContent>
				<DialogHeader>
					<DialogTitle>Drucker erstellen</DialogTitle>
				</DialogHeader>
				<PrinterForm setOpen={setOpen} />
			</DialogContent>
		</Dialog>
	);
}

================
File: src/app/admin/printers/dialogs/delete-printer.tsx
================
"use client";

import {
	AlertDialog,
	AlertDialogAction,
	AlertDialogCancel,
	AlertDialogContent,
	AlertDialogDescription,
	AlertDialogFooter,
	AlertDialogHeader,
	AlertDialogTitle,
	AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
import { deletePrinter } from "@/server/actions/printers";
import { TrashIcon } from "lucide-react";

interface DeletePrinterDialogProps {
	printerId: string;
	setOpen: (state: boolean) => void;
}
export function DeletePrinterDialog(props: DeletePrinterDialogProps) {
	const { printerId, setOpen } = props;
	const { toast } = useToast();

	async function onSubmit() {
		toast({
			description: "Drucker wird gelöscht...",
		});
		try {
			const result = await deletePrinter(printerId);
			if (result?.error) {
				toast({
					description: result.error,
					variant: "destructive",
				});
			}
			toast({
				description: "Drucker wurde gelöscht.",
			});
			setOpen(false);
		} catch (error) {
			if (error instanceof Error) {
				toast({
					description: error.message,
					variant: "destructive",
				});
			} else {
				toast({
					description: "Ein unbekannter Fehler ist aufgetreten.",
					variant: "destructive",
				});
			}
		}
	}

	return (
		<AlertDialog>
			<AlertDialogTrigger asChild>
				<Button variant="destructive" className="gap-2 flex items-center">
					<TrashIcon className="w-4 h-4" />
					<span>Drucker löschen</span>
				</Button>
			</AlertDialogTrigger>
			<AlertDialogContent>
				<AlertDialogHeader>
					<AlertDialogTitle>Bist Du dir sicher?</AlertDialogTitle>
					<AlertDialogDescription>
						Diese Aktion kann nicht rückgängig gemacht werden. Der Drucker und die damit verbundenen Daten werden
						unwiderruflich gelöscht.
					</AlertDialogDescription>
				</AlertDialogHeader>
				<AlertDialogFooter>
					<AlertDialogCancel>Abbrechen</AlertDialogCancel>
					<AlertDialogAction className="bg-red-500" onClick={onSubmit}>
						Ja, löschen
					</AlertDialogAction>
				</AlertDialogFooter>
			</AlertDialogContent>
		</AlertDialog>
	);
}

================
File: src/app/admin/printers/dialogs/edit-printer.tsx
================
import { PrinterForm } from "@/app/admin/printers/form";
import { DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import type { InferResultType } from "@/utils/drizzle";

interface EditPrinterDialogTriggerProps {
	children: React.ReactNode;
}

export function EditPrinterDialogTrigger(props: EditPrinterDialogTriggerProps) {
	const { children } = props;

	return <DialogTrigger asChild>{children}</DialogTrigger>;
}

interface EditPrinterDialogContentProps {
	printer: InferResultType<"printers">;
	setOpen: (open: boolean) => void;
}
export function EditPrinterDialogContent(props: EditPrinterDialogContentProps) {
	const { printer, setOpen } = props;

	return (
		<DialogContent>
			<DialogHeader>
				<DialogTitle>Drucker bearbeiten</DialogTitle>
			</DialogHeader>
			<PrinterForm setOpen={setOpen} printer={printer} />
		</DialogContent>
	);
}

================
File: src/app/admin/printers/form.tsx
================
"use client";
import { DeletePrinterDialog } from "@/app/admin/printers/dialogs/delete-printer";
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 { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/components/ui/use-toast";
import { createPrinter, updatePrinter } from "@/server/actions/printers";
import type { InferResultType } from "@/utils/drizzle";
import { zodResolver } from "@hookform/resolvers/zod";
import { SaveIcon, XCircleIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";

export const formSchema = z.object({
	name: z
		.string()
		.min(2, {
			message: "Der Name muss mindestens 2 Zeichen lang sein.",
		})
		.max(50),
	description: z
		.string()
		.min(2, {
			message: "Die Beschreibung muss mindestens 2 Zeichen lang sein.",
		})
		.max(50),
	status: z.coerce.number().int().min(0).max(1),
});

interface PrinterFormProps {
	printer?: InferResultType<"printers">;
	setOpen: (state: boolean) => void;
}

export function PrinterForm(props: PrinterFormProps) {
	const { printer, setOpen } = props;
	const { toast } = useToast();

	const form = useForm<z.infer<typeof formSchema>>({
		resolver: zodResolver(formSchema),
		defaultValues: {
			name: printer?.name ?? "",
			description: printer?.description ?? "",
			status: printer?.status ?? 0,
		},
	});

	// 2. Define a submit handler.
	async function onSubmit(values: z.infer<typeof formSchema>) {
		// TODO: create or update
		if (printer) {
			toast({
				description: "Drucker wird aktualisiert...",
			});

			// Update
			try {
				const result = await updatePrinter(printer.id, {
					description: values.description,
					name: values.name,
					status: values.status,
				});
				if (result?.error) {
					toast({
						description: result.error,
						variant: "destructive",
					});
				}

				setOpen(false);

				toast({
					description: "Drucker wurde aktualisiert.",
					variant: "default",
				});
			} catch (error) {
				if (error instanceof Error) {
					toast({
						description: error.message,
						variant: "destructive",
					});
				} else {
					toast({
						description: "Ein unbekannter Fehler ist aufgetreten.",
						variant: "destructive",
					});
				}
			}
		} else {
			toast({
				description: "Drucker wird erstellt...",
				variant: "default",
			});

			// Create
			try {
				const result = await createPrinter({
					description: values.description,
					name: values.name,
					status: values.status,
				});
				if (result?.error) {
					toast({
						description: result.error,
						variant: "destructive",
					});
				}

				setOpen(false);

				toast({
					description: "Drucker wurde erstellt.",
					variant: "default",
				});
			} catch (error) {
				if (error instanceof Error) {
					toast({
						description: error.message,
						variant: "destructive",
					});
				} else {
					toast({
						description: "Ein unbekannter Fehler ist aufgetreten.",
						variant: "destructive",
					});
				}
			}
		}
	}

	return (
		<Form {...form}>
			<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
				<FormField
					control={form.control}
					name="name"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Name</FormLabel>
							<FormControl>
								<Input placeholder="Anycubic Kobra 2 Pro" {...field} />
							</FormControl>
							<FormDescription>Bitte gib einen eindeutigen Namen für den Drucker ein.</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<FormField
					control={form.control}
					name="description"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Beschreibung</FormLabel>
							<FormControl>
								<Input placeholder="80x80x80 Druckfläche, langsam" {...field} />
							</FormControl>
							<FormDescription>Füge eine kurze Beschreibung des Druckers hinzu.</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<FormField
					control={form.control}
					name="status"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Status</FormLabel>
							<Select onValueChange={field.onChange} defaultValue={field.value.toString()}>
								<FormControl>
									<SelectTrigger>
										<SelectValue placeholder="Select a verified email to display" />
									</SelectTrigger>
								</FormControl>
								<SelectContent>
									<SelectItem value={"0"}>Verfügbar</SelectItem>
									<SelectItem value={"1"}>Außer Betrieb</SelectItem>
								</SelectContent>
							</Select>
							<FormDescription>Wähle den aktuellen Status des Druckers.</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<div className="flex justify-between items-center">
					{printer && <DeletePrinterDialog setOpen={setOpen} printerId={printer?.id} />}
					{!printer && (
						<DialogClose asChild>
							<Button variant="secondary" className="gap-2 flex items-center">
								<XCircleIcon className="w-4 h-4" />
								<span>Abbrechen</span>
							</Button>
						</DialogClose>
					)}
					<Button type="submit" className="gap-2 flex items-center">
						<SaveIcon className="w-4 h-4" />
						<span>Speichern</span>
					</Button>
				</div>
			</form>
		</Form>
	);
}

================
File: src/app/admin/printers/page.tsx
================
import { columns } from "@/app/admin/printers/columns";
import { DataTable } from "@/app/admin/printers/data-table";
import { CreatePrinterDialog } from "@/app/admin/printers/dialogs/create-printer";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { db } from "@/server/db";
import { PlusCircleIcon } from "lucide-react";

export default async function AdminPage() {
	const data = await db.query.printers.findMany();

	return (
		<Card>
			<CardHeader className="flex flex-row justify-between items-center">
				<div>
					<CardTitle>Druckerverwaltung</CardTitle>
					<CardDescription>Suche, Bearbeite, Lösche und Erstelle Drucker</CardDescription>
				</div>
				<CreatePrinterDialog>
					<Button variant={"default"} className="flex gap-2 items-center">
						<PlusCircleIcon className="w-4 h-4" />
						<span>Drucker erstellen</span>
					</Button>
				</CreatePrinterDialog>
			</CardHeader>
			<CardContent>
				<DataTable columns={columns} data={data} />
			</CardContent>
		</Card>
	);
}

================
File: src/app/admin/settings/download/route.ts
================
import fs from "node:fs";

export const dynamic = 'force-dynamic';

export async function GET() {
	return new Response(fs.readFileSync("./db/sqlite.db"));
}

================
File: src/app/admin/settings/page.tsx
================
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import Link from "next/link";

import type { Metadata } from "next";

export const metadata: Metadata = {
	title: "Systemeinstellungen",
};

export default function AdminPage() {
	return (
		<Card>
			<CardHeader>
				<CardTitle>Einstellungen</CardTitle>
				<CardDescription>Systemeinstellungen</CardDescription>
			</CardHeader>
			<CardContent>
				<div className="flex gap-8 items-center">
					<p>Datenbank herunterladen</p>
					<Button variant="default" asChild>
						<Link href="/admin/settings/download" target="_blank">
							Herunterladen
						</Link>
					</Button>
				</div>
			</CardContent>
		</Card>
	);
}

================
File: src/app/admin/users/columns.tsx
================
"use client";

import { type UserRole, translateUserRole } from "@/server/auth/permissions";
import type { users } from "@/server/db/schema";
import type { ColumnDef } from "@tanstack/react-table";
import type { InferSelectModel } from "drizzle-orm";
import {
	ArrowUpDown,
	MailIcon,
	MessageCircleIcon,
	MoreHorizontal,
} from "lucide-react";

import { Button } from "@/components/ui/button";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuSeparator,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import Link from "next/link";
import {
	EditUserDialogContent,
	EditUserDialogRoot,
	EditUserDialogTrigger,
} from "@/app/admin/users/dialog";

// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type User = {
	id: string;
	github_id: number;
	username: string;
	displayName: string;
	email: string;
	role: string;
};

export const columns: ColumnDef<InferSelectModel<typeof users>>[] = [
	{
		accessorKey: "id",
		header: ({ column }) => {
			return (
				<Button
					variant="ghost"
					onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
				>
					ID
					<ArrowUpDown className="ml-2 h-4 w-4" />
				</Button>
			);
		},
	},
	{
		accessorKey: "github_id",
		header: "GitHub ID",
	},
	{
		accessorKey: "username",
		header: "Username",
	},
	{
		accessorKey: "displayName",
		header: "Name",
	},
	{
		accessorKey: "email",
		header: "E-Mail",
	},
	{
		accessorKey: "role",
		header: "Rolle",
		cell: ({ row }) => {
			const role = row.getValue("role");
			const translated = translateUserRole(role as UserRole);

			return translated;
		},
	},
	{
		id: "actions",
		cell: ({ row }) => {
			const user = row.original;

			return (
				<EditUserDialogRoot>
					<DropdownMenu>
						<DropdownMenuTrigger asChild>
							<Button variant="ghost" className="h-8 w-8 p-0">
								<span className="sr-only">Menu öffnen</span>
								<MoreHorizontal className="h-4 w-4" />
							</Button>
						</DropdownMenuTrigger>
						<DropdownMenuContent align="end">
							<DropdownMenuLabel>Aktionen</DropdownMenuLabel>
							<DropdownMenuItem asChild>
								<Link
									target="_blank"
									href={generateTeamsChatURL(user.email)}
									className="flex gap-2 items-center"
								>
									<MessageCircleIcon className="w-4 h-4" />
									<span>Teams-Chat öffnen</span>
								</Link>
							</DropdownMenuItem>
							<DropdownMenuItem asChild>
								<Link
									target="_blank"
									href={generateEMailURL(user.email)}
									className="flex gap-2 items-center"
								>
									<MailIcon className="w-4 h-4" />
									<span>E-Mail schicken</span>
								</Link>
							</DropdownMenuItem>
							<DropdownMenuSeparator />
							<DropdownMenuItem>
								<EditUserDialogTrigger />
							</DropdownMenuItem>
						</DropdownMenuContent>
					</DropdownMenu>
					<EditUserDialogContent user={user as User} />
				</EditUserDialogRoot>
			);
		},
	},
];

function generateTeamsChatURL(email: string) {
	return `https://teams.microsoft.com/l/chat/0/0?users=${email}`;
}

function generateEMailURL(email: string) {
	return `mailto:${email}`;
}

================
File: src/app/admin/users/data-table.tsx
================
"use client";

import {
	type ColumnDef,
	type ColumnFiltersState,
	type SortingState,
	type VisibilityState,
	flexRender,
	getCoreRowModel,
	getFilteredRowModel,
	getPaginationRowModel,
	getSortedRowModel,
	useReactTable,
} from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
import {
	DropdownMenu,
	DropdownMenuCheckboxItem,
	DropdownMenuContent,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { SlidersHorizontalIcon } from "lucide-react";
import { useState } from "react";

interface DataTableProps<TData, TValue> {
	columns: ColumnDef<TData, TValue>[];
	data: TData[];
}

export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
	const [sorting, setSorting] = useState<SortingState>([]);
	const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
	const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});

	const table = useReactTable({
		data,
		columns,
		getCoreRowModel: getCoreRowModel(),
		getPaginationRowModel: getPaginationRowModel(),
		onSortingChange: setSorting,
		getSortedRowModel: getSortedRowModel(),
		onColumnFiltersChange: setColumnFilters,
		getFilteredRowModel: getFilteredRowModel(),
		onColumnVisibilityChange: setColumnVisibility,
		state: {
			sorting,
			columnFilters,
			columnVisibility,
		},
	});

	return (
		<div>
			<div className="flex items-center py-4">
				<Input
					placeholder="E-Mails filtern..."
					value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
					onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value)}
					className="max-w-sm"
				/>
				<DropdownMenu>
					<DropdownMenuTrigger asChild>
						<Button variant="outline" className="ml-auto flex items-center gap-2">
							<SlidersHorizontalIcon className="h-4 w-4" />
							<span>Spalten</span>
						</Button>
					</DropdownMenuTrigger>
					<DropdownMenuContent align="end">
						{table
							.getAllColumns()
							.filter((column) => column.getCanHide())
							.map((column) => {
								return (
									<DropdownMenuCheckboxItem
										key={column.id}
										className="capitalize"
										checked={column.getIsVisible()}
										onCheckedChange={(value) => column.toggleVisibility(!!value)}
									>
										{column.id}
									</DropdownMenuCheckboxItem>
								);
							})}
					</DropdownMenuContent>
				</DropdownMenu>
			</div>

			<div className="rounded-md border">
				<Table>
					<TableHeader>
						{table.getHeaderGroups().map((headerGroup) => (
							<TableRow key={headerGroup.id}>
								{headerGroup.headers.map((header) => {
									return (
										<TableHead key={header.id}>
											{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
										</TableHead>
									);
								})}
							</TableRow>
						))}
					</TableHeader>
					<TableBody>
						{table.getRowModel().rows?.length ? (
							table.getRowModel().rows.map((row) => (
								<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
									{row.getVisibleCells().map((cell) => (
										<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
									))}
								</TableRow>
							))
						) : (
							<TableRow>
								<TableCell colSpan={columns.length} className="h-24 text-center">
									Keine Ergebnisse gefunden.
								</TableCell>
							</TableRow>
						)}
					</TableBody>
				</Table>
			</div>
			<div className="flex items-center justify-end space-x-2 py-4">
				<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
					Zurück
				</Button>
				<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
					Nächste Seite
				</Button>
			</div>
		</div>
	);
}

================
File: src/app/admin/users/dialog.tsx
================
import type { User } from "@/app/admin/users/columns";
import { ProfileForm } from "@/app/admin/users/form";
import {
	Dialog,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
	DialogTrigger,
} from "@/components/ui/dialog";
import { PencilIcon } from "lucide-react";

interface EditUserDialogRootProps {
	children: React.ReactNode;
}

export function EditUserDialogRoot(props: EditUserDialogRootProps) {
	const { children } = props;

	return <Dialog>{children}</Dialog>;
}

export function EditUserDialogTrigger() {
	return (
		<DialogTrigger className="flex gap-2 items-center">
			<PencilIcon className="w-4 h-4" />
			<span>Benutzer bearbeiten</span>
		</DialogTrigger>
	);
}

interface EditUserDialogContentProps {
	user: User;
}

export function EditUserDialogContent(props: EditUserDialogContentProps) {
	const { user } = props;

	if (!user) {
		return;
	}

	return (
		<DialogContent>
			<DialogHeader>
				<DialogTitle>Benutzer bearbeiten</DialogTitle>
				<DialogDescription>
					<strong>Hinweis:</strong> In den seltensten Fällen sollten die Daten
					eines Benutzers geändert werden. Dies kann zu unerwarteten Problemen
					führen.
				</DialogDescription>
			</DialogHeader>
			<ProfileForm user={user} />
		</DialogContent>
	);
}

================
File: src/app/admin/users/form.tsx
================
"use client";

import type { User } from "@/app/admin/users/columns";
import {
	AlertDialog,
	AlertDialogAction,
	AlertDialogCancel,
	AlertDialogContent,
	AlertDialogDescription,
	AlertDialogFooter,
	AlertDialogHeader,
	AlertDialogTitle,
	AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
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 { Input } from "@/components/ui/input";
import {
	Select,
	SelectContent,
	SelectItem,
	SelectTrigger,
	SelectValue,
} from "@/components/ui/select";
import { useToast } from "@/components/ui/use-toast";
import { deleteUser, updateUser } from "@/server/actions/users";
import type { UserRole } from "@/server/auth/permissions";
import { zodResolver } from "@hookform/resolvers/zod";
import { SaveIcon, TrashIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { z } from "zod";

export const formSchema = z.object({
	username: z
		.string()
		.min(2, {
			message: "Der Benutzername muss mindestens 2 Zeichen lang sein.",
		})
		.max(50),
	displayName: z
		.string()
		.min(2, {
			message: "Der Anzeigename muss mindestens 2 Zeichen lang sein.",
		})
		.max(50),
	email: z.string().email(),
	role: z.enum(["admin", "user", "guest"]),
});

interface ProfileFormProps {
	user: User;
}

export function ProfileForm(props: ProfileFormProps) {
	const { user } = props;
	const { toast } = useToast();

	// 1. Define your form.
	const form = useForm<z.infer<typeof formSchema>>({
		resolver: zodResolver(formSchema),
		defaultValues: {
			username: user.username,
			displayName: user.displayName,
			email: user.email,
			role: user.role as UserRole,
		},
	});

	// 2. Define a submit handler.
	async function onSubmit(values: z.infer<typeof formSchema>) {
		toast({ description: "Benutzerprofil wird aktualisiert..." });

		await updateUser(user.id, values);

		toast({ description: "Benutzerprofil wurde aktualisiert." });
	}

	return (
		<Form {...form}>
			<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
				<FormField
					control={form.control}
					name="username"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Benutzername</FormLabel>
							<FormControl>
								<Input placeholder="MAXMUS" {...field} />
							</FormControl>
							<FormDescription>
								Nur in Ausnahmefällen sollte der Benutzername geändert werden.
							</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<FormField
					control={form.control}
					name="displayName"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Anzeigename</FormLabel>
							<FormControl>
								<Input placeholder="Max Mustermann" {...field} />
							</FormControl>
							<FormDescription>
								Der Anzeigename darf frei verändert werden.
							</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<FormField
					control={form.control}
					name="email"
					render={({ field }) => (
						<FormItem>
							<FormLabel>E-Mail Adresse</FormLabel>
							<FormControl>
								<Input
									placeholder="max.mustermann@mercedes-benz.com"
									{...field}
								/>
							</FormControl>
							<FormDescription>
								Nur in Ausnahmefällen sollte die E-Mail Adresse geändert werden.
							</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<FormField
					control={form.control}
					name="role"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Benutzerrolle</FormLabel>
							<Select onValueChange={field.onChange} defaultValue={field.value}>
								<FormControl>
									<SelectTrigger>
										<SelectValue placeholder="Select a verified email to display" />
									</SelectTrigger>
								</FormControl>
								<SelectContent>
									<SelectItem value="admin">Administrator</SelectItem>
									<SelectItem value="user">Benutzer</SelectItem>
									<SelectItem value="guest">Gast</SelectItem>
								</SelectContent>
							</Select>
							<FormDescription>
								Die Benutzerrolle bestimmt die Berechtigungen des Benutzers.
							</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<div className="flex justify-between items-center">
					<AlertDialog>
						<AlertDialogTrigger asChild>
							<Button
								type="submit"
								variant="destructive"
								className="gap-2 flex items-center"
							>
								<TrashIcon className="w-4 h-4" />
								<span>Benutzer löschen</span>
							</Button>
						</AlertDialogTrigger>
						<AlertDialogContent>
							<AlertDialogHeader>
								<AlertDialogTitle>Bist du dir sicher?</AlertDialogTitle>
								<AlertDialogDescription>
									Diese Aktion kann nicht rückgängig gemacht werden. Das
									Benutzerprofil und die damit verbundenen Daten werden
									unwiderruflich gelöscht.
								</AlertDialogDescription>
							</AlertDialogHeader>
							<AlertDialogFooter>
								<AlertDialogCancel>Abbrechen</AlertDialogCancel>
								<AlertDialogAction
									className="bg-red-500"
									onClick={() => {
										toast({ description: "Benutzerprofil wird gelöscht..." });
										deleteUser(user.id);
										toast({ description: "Benutzerprofil wurde gelöscht." });
									}}
								>
									Ja, löschen
								</AlertDialogAction>
							</AlertDialogFooter>
						</AlertDialogContent>
					</AlertDialog>
					<DialogClose asChild>
						<Button type="submit" className="gap-2 flex items-center">
							<SaveIcon className="w-4 h-4" />
							<span>Speichern</span>
						</Button>
					</DialogClose>
				</div>
			</form>
		</Form>
	);
}

================
File: src/app/admin/users/page.tsx
================
import { columns } from "@/app/admin/users/columns";
import { DataTable } from "@/app/admin/users/data-table";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { db } from "@/server/db";

import type { Metadata } from "next";

export const metadata: Metadata = {
	title: "Alle Benutzer",
};

export default async function AdminPage() {
	const data = await db.query.users.findMany();

	return (
		<Card>
			<CardHeader>
				<CardTitle>Benutzerverwaltung</CardTitle>
				<CardDescription>Suche, Bearbeite und Lösche Benutzer</CardDescription>
			</CardHeader>
			<CardContent>
				<DataTable columns={columns} data={data} />
			</CardContent>
		</Card>
	);
}

================
File: src/app/api/job/[jobId]/remaining-time/route.ts
================
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),
	});

	// Check if the job exists
	if (!jobDetails) {
		return Response.json({
			id: params.jobId,
			error: "Job not found",
		});
	}

	// Calculate the remaining time
	const startAt = new Date(jobDetails.startAt).getTime();
	const endAt = startAt + jobDetails.durationInMinutes * 60 * 1000;
	const remainingTime = Math.max(0, endAt - Date.now());

	// Return the remaining time
	return Response.json({
		id: params.jobId,
		remainingTime,
	});
}

================
File: src/app/api/printers/route.ts
================
import { getPrinters } from "@/server/actions/printers";

export const dynamic = "force-dynamic";

export async function GET() {
	const printers = await getPrinters();

	return Response.json(printers);
}

================
File: src/app/auth/login/callback/route.ts
================
import { lucia } from "@/server/auth";
import { type GitHubUserResult, github } from "@/server/auth/oauth";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { OAuth2RequestError } from "arctic";
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,
			},
		);
	}

	try {
		const tokens = await github.validateAuthorizationCode(code);
		const githubUserResponse = await fetch("https://git.i.mercedes-benz.com/api/v3/user", {
			headers: {
				Authorization: `Bearer ${tokens.accessToken}`,
			},
		});
		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;
		}
		const existingUser = await db.query.users.findFirst({
			where: eq(users.github_id, githubUser.id),
		});

		if (existingUser) {
			const session = await lucia.createSession(existingUser.id, {});
			const sessionCookie = lucia.createSessionCookie(session.id);
			cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
			return new Response(null, {
				status: 302,
				headers: {
					Location: "/",
				},
			});
		}

		const userId = generateIdFromEntropySize(10); // 16 characters long

		await db.insert(users).values({
			id: userId,
			github_id: githubUser.id,
			username: githubUser.login,
			displayName: githubUser.name,
			email: githubUser.email,
		});

		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
		});
		return new Response(null, {
			status: 302,
			headers: {
				Location: "/",
			},
		});
	} catch (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: 500,
		});
	}
}

================
File: src/app/auth/login/route.ts
================
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 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
		httpOnly: true,
		maxAge: ONE_HOUR,
		sameSite: "lax",
	});

	return Response.redirect(url);
}

================
File: src/app/globals.css
================
@tailwind base;
@tailwind components;
@tailwind utilities;


@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.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%;
    --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%;
  }

  .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%;
    --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%;
  }
}

================
File: src/app/job/[jobId]/cancel-form.tsx
================
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
	Dialog,
	DialogClose,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
	DialogTrigger,
} from "@/components/ui/dialog";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/use-toast";
import { abortPrintJob } from "@/server/actions/printJobs";
import { TriangleAlertIcon } from "lucide-react";
import { useState } from "react";

const formSchema = z.object({
	abortReason: z
		.string()
		.min(1, {
			message: "Bitte gebe einen Grund für den Abbruch an.",
		})
		.max(255, {
			message: "Der Grund darf maximal 255 Zeichen lang sein.",
		}),
});

interface CancelFormProps {
	jobId: string;
}

export function CancelForm(props: CancelFormProps) {
	const { jobId } = props;
	const form = useForm<z.infer<typeof formSchema>>({
		resolver: zodResolver(formSchema),
		defaultValues: {
			abortReason: "",
		},
	});
	const { toast } = useToast();
	const [open, setOpen] = useState(false);

	async function onSubmit(values: z.infer<typeof formSchema>) {
		toast({
			description: "Druckauftrag wird abgebrochen...",
		});
		try {
			const result = await abortPrintJob(jobId, values.abortReason);
			if (result?.error) {
				toast({
					description: result.error,
					variant: "destructive",
				});
			}
			setOpen(false);
			toast({
				description: "Druckauftrag wurde abgebrochen.",
			});
		} catch (error) {
			if (error instanceof Error) {
				toast({
					description: error.message,
					variant: "destructive",
				});
			} else {
				toast({
					description: "Ein unbekannter Fehler ist aufgetreten.",
					variant: "destructive",
				});
			}
		}
	}

	return (
		<Dialog open={open} onOpenChange={setOpen}>
			<DialogTrigger asChild>
				<Button
					variant={"ghost"}
					className="text-red-500 hover:text-red-600 flex-grow gap-2 items-center flex justify-start"
				>
					<TriangleAlertIcon className="w-4 h-4" />
					<span>Druckauftrag abbrechen</span>
				</Button>
			</DialogTrigger>
			<DialogContent>
				<DialogHeader>
					<DialogTitle>Druckauftrag abbrechen?</DialogTitle>
					<DialogDescription>
						Du bist dabei, den Druckauftrag abzubrechen. Bitte beachte, dass ein abgebrochener Druckauftrag nicht wieder
						aufgenommen werden kann und der Drucker sich automatisch abschaltet.
					</DialogDescription>
				</DialogHeader>
				<Form {...form}>
					<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
						<FormField
							control={form.control}
							name="abortReason"
							render={({ field }) => (
								<FormItem>
									<FormLabel>Grund für den Abbruch</FormLabel>
									<FormControl>
										<Input {...field} />
									</FormControl>
									<FormDescription>
										Bitte teile uns den Grund für den Abbruch des Druckauftrags mit. Wenn der Drucker eine Fehlermeldung
										anzeigt, gib bitte nur diese Fehlermeldung an.
									</FormDescription>
									<FormMessage />
								</FormItem>
							)}
						/>
						<div className="flex flex-row justify-between">
							<DialogClose asChild>
								<Button variant={"secondary"}>Nein</Button>
							</DialogClose>
							<Button variant={"destructive"} type="submit">
								Ja, Druck abbrechen
							</Button>
						</div>
					</form>
				</Form>
			</DialogContent>
		</Dialog>
	);
}

================
File: src/app/job/[jobId]/edit-comments.tsx
================
"use client";

import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/use-toast";
import { updatePrintComments } from "@/server/actions/printJobs";
import { useDebouncedCallback } from "use-debounce";

interface EditCommentsProps {
	defaultValue: string | null;
	jobId: string;
	disabled?: boolean;
}
export function EditComments(props: EditCommentsProps) {
	const { defaultValue, jobId, disabled } = props;
	const { toast } = useToast();

	const debounced = useDebouncedCallback(async (value) => {
		try {
			const result = await updatePrintComments(jobId, value);
			if (result?.error) {
				toast({
					description: result.error,
					variant: "destructive",
				});
			}
			toast({
				description: "Anmerkungen wurden gespeichert.",
			});
		} catch (error) {
			if (error instanceof Error) {
				toast({
					description: error.message,
					variant: "destructive",
				});
			} else {
				toast({
					description: "Ein unbekannter Fehler ist aufgetreten.",
					variant: "destructive",
				});
			}
		}
	}, 1000);

	return (
		<div className="flex flex-col gap-2">
			<Label>Anmerkungen</Label>
			<Textarea
				placeholder="Anmerkungen"
				disabled={disabled}
				defaultValue={defaultValue ?? ""}
				onChange={(e) => debounced(e.target.value)}
			/>
		</div>
	);
}

================
File: src/app/job/[jobId]/extend-form.tsx
================
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
	Dialog,
	DialogClose,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
	DialogTrigger,
} from "@/components/ui/dialog";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/use-toast";
import { extendPrintJob } from "@/server/actions/printJobs";
import { CircleFadingPlusIcon } from "lucide-react";
import { useState } from "react";
import { useSWRConfig } from "swr";

const formSchema = z.object({
	minutes: z.coerce.number().int().max(59, {
		message: "Die Minuten müssen zwischen 0 und 59 liegen.",
	}),
	hours: z.coerce.number().int().max(24, {
		message: "Die Stunden müssen zwischen 0 und 24 liegen.",
	}),
});

interface ExtendFormProps {
	jobId: string;
}

export function ExtendForm(props: ExtendFormProps) {
	const { jobId } = props;
	const form = useForm<z.infer<typeof formSchema>>({
		resolver: zodResolver(formSchema),
		defaultValues: {
			minutes: 0,
			hours: 0,
		},
	});
	const { toast } = useToast();
	const [open, setOpen] = useState(false);
	const { mutate } = useSWRConfig();

	async function onSubmit(values: z.infer<typeof formSchema>) {
		toast({
			description: "Druckauftrag wird verlängert...",
		});
		try {
			const result = await extendPrintJob(jobId, values.minutes, values.hours);

			if (result?.error) {
				toast({
					description: result.error,
					variant: "destructive",
				});
			}

			setOpen(false);
			form.reset();

			mutate(`/api/job/${jobId}/remaining-time`); // Refresh the countdown

			toast({
				description: "Druckauftrag wurde verlängert.",
			});
		} catch (error) {
			if (error instanceof Error) {
				toast({
					description: error.message,
					variant: "destructive",
				});
			} else {
				toast({
					description: "Ein unbekannter Fehler ist aufgetreten.",
					variant: "destructive",
				});
			}
		}
	}

	return (
		<Dialog open={open} onOpenChange={setOpen}>
			<DialogTrigger asChild>
				<Button variant={"ghost"} className="flex-grow gap-2 items-center flex justify-start">
					<CircleFadingPlusIcon className="w-4 h-4" />
					<span>Druckauftrag verlängern</span>
				</Button>
			</DialogTrigger>
			<DialogContent>
				<DialogHeader>
					<DialogTitle>Druckauftrag verlängern</DialogTitle>
					<DialogDescription>
						Braucht dein Druck mehr Zeit als erwartet? Füge weitere Stunden oder Minuten zur Druckzeit hinzu.
					</DialogDescription>
				</DialogHeader>
				<Form {...form}>
					<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
						<p className="text-sm px-4 py-2 text-yellow-700 bg-yellow-500/20 rounded-md">
							<span className="font-medium">Wichtig:</span> Bitte verlängere die Druckzeit nur, wenn es sich um
							denselben Druck handelt. Wenn es ein anderer Druck ist, brich bitte den aktuellen Druckauftrag ab und
							starte einen neuen.
						</p>
						<div className="flex flex-row gap-2">
							<FormField
								control={form.control}
								name="hours"
								render={({ field }) => (
									<FormItem className="w-1/2">
										<FormLabel>Stunden</FormLabel>
										<FormControl>
											<Input placeholder="0" {...field} />
										</FormControl>
										<FormMessage />
									</FormItem>
								)}
							/>
							<FormField
								control={form.control}
								name="minutes"
								render={({ field }) => (
									<FormItem className="w-1/2">
										<FormLabel>Minuten</FormLabel>
										<FormControl>
											<Input placeholder="0" {...field} />
										</FormControl>
										<FormMessage />
									</FormItem>
								)}
							/>
						</div>
						<div className="flex flex-row justify-between">
							<DialogClose asChild>
								<Button variant={"secondary"}>Abbrechen</Button>
							</DialogClose>
							<Button variant={"default"} type="submit">
								Verlängern
							</Button>
						</div>
					</form>
				</Form>
			</DialogContent>
		</Dialog>
	);
}

================
File: src/app/job/[jobId]/finish-form.tsx
================
"use client";

import { AlertDialogHeader } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
	Dialog,
	DialogClose,
	DialogContent,
	DialogDescription,
	DialogTitle,
	DialogTrigger,
} from "@/components/ui/dialog";
import { useToast } from "@/components/ui/use-toast";
import { earlyFinishPrintJob } from "@/server/actions/printJobs";
import { CircleCheckBigIcon } from "lucide-react";

interface FinishFormProps {
	jobId: string;
}

export function FinishForm(props: FinishFormProps) {
	const { jobId } = props;
	const { toast } = useToast();

	async function onClick() {
		toast({
			description: "Druckauftrag wird abgeschlossen...",
		});
		try {
			const result = await earlyFinishPrintJob(jobId);
			if (result?.error) {
				toast({
					description: result.error,
					variant: "destructive",
				});
			}
			toast({
				description: "Druckauftrag wurde abgeschlossen.",
			});
		} catch (error) {
			if (error instanceof Error) {
				toast({
					description: error.message,
					variant: "destructive",
				});
			} else {
				toast({
					description: "Ein unbekannter Fehler ist aufgetreten.",
					variant: "destructive",
				});
			}
		}
	}

	return (
		<Dialog>
			<DialogTrigger asChild>
				<Button variant={"ghost"} className="flex-grow gap-2 items-center flex justify-start">
					<CircleCheckBigIcon className="w-4 h-4" />
					<span>Druckauftrag abschließen</span>
				</Button>
			</DialogTrigger>
			<DialogContent>
				<AlertDialogHeader>
					<DialogTitle>Druckauftrag abschließen?</DialogTitle>
					<DialogDescription>
						Du bist dabei, den Druckauftrag als abgeschlossen zu markieren. Dies führt dazu, dass der Drucker
						automatisch herunterfährt.
					</DialogDescription>
				</AlertDialogHeader>
				<div className="flex flex-col gap-4">
					<p className="text-sm text-red-500 font-medium bg-red-500/20 px-4 py-2 rounded-md">
						Bitte bestätige nur, wenn der Druckauftrag tatsächlich erfolgreich abgeschlossen wurde.
					</p>
					<div className="flex flex-row justify-between">
						<DialogClose asChild>
							<Button variant={"secondary"}>Abbrechen</Button>
						</DialogClose>
						<DialogClose asChild onClick={onClick}>
							<Button variant={"default"}>Bestätigen</Button>
						</DialogClose>
					</div>
				</div>
			</DialogContent>
		</Dialog>
	);
}

================
File: src/app/job/[jobId]/page.tsx
================
import { CancelForm } from "@/app/job/[jobId]/cancel-form";
import { EditComments } from "@/app/job/[jobId]/edit-comments";
import { ExtendForm } from "@/app/job/[jobId]/extend-form";
import { FinishForm } from "@/app/job/[jobId]/finish-form";
import { Countdown } from "@/components/printer-card/countdown";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { printJobs } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { ArchiveIcon } from "lucide-react";

import type { Metadata } from "next";

export const metadata: Metadata = {
	title: "Druckauftrag",
};

interface JobDetailsPageProps {
	params: {
		jobId: string;
	};
}
export default async function JobDetailsPage(props: JobDetailsPageProps) {
	const { jobId } = props.params;
	const { user } = await validateRequest();

	const jobDetails = await db.query.printJobs.findFirst({
		where: eq(printJobs.id, jobId),
		with: {
			user: true,
			printer: true,
		},
	});

	if (!jobDetails) {
		return <div>Druckauftrag wurde nicht gefunden.</div>;
	}

	const jobIsOnGoing = new Date(jobDetails.startAt).getTime() + jobDetails.durationInMinutes * 60 * 1000 > Date.now();
	const jobIsAborted = jobDetails.aborted;
	const userOwnsJob = jobDetails.userId === user?.id;
	const userIsAdmin = user?.role === UserRole.ADMIN;
	const userMayEditJob = userOwnsJob || userIsAdmin;

	return (
		<div className="flex flex-col gap-4">
			<h1 className="text-3xl font-semibold">
				Druckauftrag vom{" "}
				{new Date(jobDetails.startAt).toLocaleString("de-DE", {
					dateStyle: "medium",
					timeStyle: "medium",
				})}
			</h1>
			{!jobIsOnGoing || jobIsAborted ? (
				<Alert className="bg-yellow-200 border-yellow-500 text-yellow-700 shadow-sm">
					<ArchiveIcon className="h-4 w-4" />
					<AlertTitle>Hinweis</AlertTitle>
					<AlertDescription>
						Dieser Druckauftrag wurde bereits abgeschlossen und kann nicht mehr bearbeitet werden.
					</AlertDescription>
				</Alert>
			) : null}
			<div className="flex flex-col lg:flex-row gap-4">
				<Card className="w-full">
					<CardContent className="p-4 flex flex-col gap-4">
						<div className="flex flex-row justify-between">
							<div>
								<h2 className="font-semibold">Ansprechpartner</h2>
								<p className="text-sm">{jobDetails.user.displayName}</p>
								<p className="text-sm">{jobDetails.user.email}</p>
							</div>
							<div className="text-right">
								{jobIsAborted && (
									<>
										<h2 className="font-semibold text-red-500">Abbruchsgrund</h2>
										<p className="text-sm text-red-500">{jobDetails.abortReason}</p>
									</>
								)}
								{jobIsOnGoing && (
									<>
										<h2 className="font-semibold">Verbleibende Zeit</h2>
										<p className="text-sm">
											<Countdown jobId={jobDetails.id} />
										</p>
									</>
								)}
							</div>
						</div>
						<EditComments
							defaultValue={jobDetails.comments}
							jobId={jobDetails.id}
							disabled={!userMayEditJob || jobIsAborted || !jobIsOnGoing}
						/>
					</CardContent>
				</Card>
				{userMayEditJob && jobIsOnGoing && (
					<Card className="w-full lg:w-96 ml-auto">
						<CardHeader>
							<CardTitle>Aktionen</CardTitle>
						</CardHeader>
						<CardContent>
							<div className="flex w-full flex-col -ml-4 -mt-2">
								<FinishForm jobId={jobDetails.id} />
								<ExtendForm jobId={jobDetails.id} />
								<CancelForm jobId={jobDetails.id} />
							</div>
						</CardContent>
					</Card>
				)}
			</div>
		</div>
	);
}

/**
 * 	durationInMinutes: integer("durationInMinutes").notNull(),
		comments: text("comments"),
		aborted: integer("aborted", { mode: "boolean" }).notNull().default(false),
		abortReason: text("abortReason"),
 */

================
File: src/app/layout.tsx
================
import { Header } from "@/components/header";
import { Toaster } from "@/components/ui/toaster";
import type { Metadata } from "next";

import "@/app/globals.css";

export const metadata: Metadata = {
	title: {
		default: "MYP",
		template: "%s | MYP",
	},
	description: "Generated by create next app",
};

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"}>
				<Header />
				<main className="flex-grow max-w-screen-2xl w-full mx-auto flex flex-col p-8 gap-4 text-foreground">
					{children}
				</main>
				<Toaster />
			</body>
		</html>
	);
}

================
File: src/app/my/jobs/columns.tsx
================
"use client";

import type { InferResultType } from "@/utils/drizzle";
import type { ColumnDef } from "@tanstack/react-table";
import { BadgeCheckIcon, EyeIcon, HourglassIcon, MoreHorizontal, OctagonXIcon, ShareIcon } from "lucide-react";

import { Button } from "@/components/ui/button";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuSeparator,
	DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useToast } from "@/components/ui/use-toast";
import type { printers } from "@/server/db/schema";
import type { InferSelectModel } from "drizzle-orm";
import Link from "next/link";

export const columns: ColumnDef<
	InferResultType<
		"printJobs",
		{
			printer: true;
		}
	>
>[] = [
	{
		accessorKey: "printer",
		header: "Drucker",
		cell: ({ row }) => {
			const printer: InferSelectModel<typeof printers> = row.getValue("printer");
			return printer.name;
		},
	},
	{
		accessorKey: "startAt",
		header: "Startzeitpunkt",
		cell: ({ row }) => {
			const startAt = new Date(row.original.startAt);

			return `${startAt.toLocaleDateString("de-DE", {
				dateStyle: "medium",
			})} ${startAt.toLocaleTimeString("de-DE")}`;
		},
	},
	{
		accessorKey: "durationInMinutes",
		header: "Dauer (Minuten)",
	},
	{
		accessorKey: "comments",
		header: "Anmerkungen",
		cell: ({ row }) => {
			const comments = row.original.comments;

			if (comments) {
				return <span className="text-sm">{comments.slice(0, 50)}</span>;
			}

			return <span className="text-muted-foreground text-sm">Keine Anmerkungen</span>;
		},
	},
	{
		accessorKey: "status",
		header: "Status",
		cell: ({ row }) => {
			const aborted = row.original.aborted;

			if (aborted) {
				return (
					<div className="flex items-center gap-2">
						<OctagonXIcon className="w-4 h-4 text-red-500" /> <span className="text-red-600">Abgebrochen</span>
					</div>
				);
			}

			const startAt = new Date(row.original.startAt).getTime();
			const endAt = startAt + row.original.durationInMinutes * 60 * 1000;

			if (Date.now() < endAt) {
				return (
					<div className="flex items-center gap-2">
						<HourglassIcon className="w-4 h-4 text-yellow-500" />
						<span className="text-yellow-600">Läuft...</span>
					</div>
				);
			}

			return (
				<div className="flex items-center gap-2">
					<BadgeCheckIcon className="w-4 h-4 text-green-500" />
					<span className="text-green-600">Abgeschlossen</span>
				</div>
			);
		},
	},
	{
		id: "actions",
		cell: ({ row }) => {
			const job = row.original;
			const { toast } = useToast();

			return (
				<DropdownMenu>
					<DropdownMenuTrigger asChild>
						<Button variant="ghost" className="h-8 w-8 p-0">
							<span className="sr-only">Menu öffnen</span>
							<MoreHorizontal className="h-4 w-4" />
						</Button>
					</DropdownMenuTrigger>
					<DropdownMenuContent align="end">
						<DropdownMenuLabel>Aktionen</DropdownMenuLabel>
						<DropdownMenuItem
							className="flex items-center gap-2"
							onClick={() => {
								const baseUrl = new URL(window.location.href);
								baseUrl.pathname = `/job/${job.id}`;
								navigator.clipboard.writeText(baseUrl.toString());
								toast({
									description: "URL zum Druckauftrag in die Zwischenablage kopiert.",
								});
							}}
						>
							<ShareIcon className="w-4 h-4" />
							<span>Teilen</span>
						</DropdownMenuItem>
						<DropdownMenuSeparator />
						<DropdownMenuItem>
							<Link href={`/job/${job.id}`} className="flex items-center gap-2">
								<EyeIcon className="w-4 h-4" />
								<span>Details anzeigen</span>
							</Link>
						</DropdownMenuItem>
					</DropdownMenuContent>
				</DropdownMenu>
			);
		},
	},
];

================
File: src/app/my/jobs/data-table.tsx
================
"use client";

import {
	type ColumnDef,
	flexRender,
	getCoreRowModel,
	getPaginationRowModel,
	useReactTable,
} from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";

interface DataTableProps<TData, TValue> {
	columns: ColumnDef<TData, TValue>[];
	data: TData[];
}

export function JobsTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
	const table = useReactTable({
		data,
		columns,
		getCoreRowModel: getCoreRowModel(),
		getPaginationRowModel: getPaginationRowModel(),
	});

	return (
		<div>
			<div className="rounded-md border">
				<Table>
					<TableHeader>
						{table.getHeaderGroups().map((headerGroup) => (
							<TableRow key={headerGroup.id}>
								{headerGroup.headers.map((header) => {
									return (
										<TableHead key={header.id}>
											{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
										</TableHead>
									);
								})}
							</TableRow>
						))}
					</TableHeader>
					<TableBody>
						{table.getRowModel().rows?.length ? (
							table.getRowModel().rows.map((row) => (
								<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
									{row.getVisibleCells().map((cell) => (
										<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
									))}
								</TableRow>
							))
						) : (
							<TableRow>
								<TableCell colSpan={columns.length} className="h-24 text-center">
									Keine Ergebnisse gefunden
								</TableCell>
							</TableRow>
						)}
					</TableBody>
				</Table>
			</div>
			<div className="flex items-center justify-end space-x-2 py-4 select-none">
				<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
					Vorherige Seite
				</Button>
				<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
					Nächste Seite
				</Button>
			</div>
		</div>
	);
}

================
File: src/app/my/profile/page.tsx
================
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { validateRequest } from "@/server/auth";
import { UserRole, translateUserRole } from "@/server/auth/permissions";
import type { Metadata } from "next";
import { redirect } from "next/navigation";

export const metadata: Metadata = {
	title: "Dein Profil",
};

export default async function ProfilePage() {
	const { user } = await validateRequest();

	if (!user) {
		redirect("/");
	}

	const badgeVariant = {
		[UserRole.ADMIN]: "destructive" as const,
		[UserRole.USER]: "default" as const,
		[UserRole.GUEST]: "secondary" as const,
	};

	return (
		<Card>
			<CardHeader className="flex flex-row justify-between items-center">
				<div>
					<CardTitle>{user?.displayName}</CardTitle>
					<CardDescription>
						{user?.username} &mdash; {user?.email}
					</CardDescription>
				</div>
				<Badge variant={badgeVariant[user?.role]}>{translateUserRole(user?.role)}</Badge>
			</CardHeader>
			<CardContent>
				<p>
					Deine Daten wurden vom <abbr>GitHub Enterprise Server</abbr> importiert und können hier nur angezeigt werden.
				</p>
				<p>
					Solltest Du Änderungen oder eine Löschung deiner Daten von unserem Dienst beantragen wollen, so wende dich
					bitte an einen Administrator.
				</p>
			</CardContent>
		</Card>
	);
}

================
File: src/app/not-found.tsx
================
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>
	);
}

================
File: src/app/page.tsx
================
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 { 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 = {
	title: "Dashboard | MYP",
};

export default async function HomePage() {
	const { user } = await validateRequest();
	const userIsLoggedIn = Boolean(user);

	const printers = await db.query.printers.findMany({
		with: {
			printJobs: {
				limit: 1,
				orderBy: (printJobs, { desc }) => [desc(printJobs.startAt)],
			},
		},
	});

	// biome-ignore lint/suspicious/noExplicitAny: temp. fix for jobs
	let jobs: any[] = [];
	if (userIsLoggedIn) {
		jobs = await db.query.printJobs.findMany({
			// biome-ignore lint/style/noNonNullAssertion: User exists if userIsLoggedIn is true
			where: eq(printJobs.userId, user!.id),
			orderBy: [desc(printJobs.startAt)],
			with: {
				printer: true,
			},
		});
	}

	return (
		<>
			{/* 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>
				</CardHeader>
				<CardContent className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
					<DynamicPrinterCards user={user} />
				</CardContent>
			</Card>
			{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>
					</CardHeader>
					<CardContent>
						<JobsTable columns={columns} data={jobs} />
					</CardContent>
				</Card>
			)}
		</>
	);
}

================
File: src/app/printer/[printerId]/reserve/form.tsx
================
"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 { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/use-toast";
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";

export const formSchema = z.object({
	hours: z.coerce.number().int().min(0).max(96, {
		message: "Die Stunden müssen zwischen 0 und 96 liegen.",
	}),
	minutes: z.coerce.number().int().min(0).max(59, {
		message: "Die Minuten müssen zwischen 0 und 59 liegen.",
	}),
	comments: z.string().optional(),
});

interface PrinterReserveFormProps {
	userId: string;
	printerId: string;
	isDialog?: boolean;
}

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),
		defaultValues: {
			hours: 0,
			minutes: 0,
			comments: "",
		},
	});

	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.",
			});
			return;
		}

		try {
			const jobId = await createPrintJob({
				durationInMinutes: values.hours * 60 + values.minutes,
				comments: values.comments,
				userId: userId,
				printerId: printerId,
			});
			if (typeof jobId === "object") {
				toast({
					description: jobId.error,
					variant: "destructive",
				});
			}

			router.push(`/job/${jobId}`);
		} catch (error) {
			if (error instanceof Error) {
				toast({ variant: "destructive", description: error.message });
			} else {
				toast({
					variant: "destructive",
					description: "Ein unbekannter Fehler ist aufgetreten.",
				});
			}
			return;
		}

		toast({ description: "Druckauftrag wurde erfolgreich erstellt." });
	}

	return (
		<Form {...form}>
			<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
				<div className="flex flex-row gap-2">
					<FormField
						control={form.control}
						name="hours"
						render={({ field }) => (
							<FormItem className="w-1/2">
								<FormLabel>Stunden</FormLabel>
								<FormControl>
									<Input placeholder="0" {...field} />
								</FormControl>
								<FormMessage />
							</FormItem>
						)}
					/>
					<FormField
						control={form.control}
						name="minutes"
						render={({ field }) => (
							<FormItem className="w-1/2">
								<FormLabel>Minuten</FormLabel>
								<FormControl>
									<Input placeholder="0" {...field} />
								</FormControl>
								<FormMessage />
							</FormItem>
						)}
					/>
				</div>
				<FormField
					control={form.control}
					name="comments"
					render={({ field }) => (
						<FormItem>
							<FormLabel>Anmerkungen</FormLabel>
							<FormControl>
								<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.
							</FormDescription>
							<FormMessage />
						</FormItem>
					)}
				/>
				<div className="flex justify-between items-center">
					<If condition={isDialog}>
						<Then>
							<DialogClose asChild>
								<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}>
						<CalendarPlusIcon className="w-4 h-4" />
						<span>Reservieren</span>
					</Button>
				</div>
			</form>
		</Form>
	);
}

================
File: src/app/printer/[printerId]/reserve/page.tsx
================
import { PrinterReserveForm } from "@/app/printer/[printerId]/reserve/form";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { validateRequest } from "@/server/auth";
import { redirect } from "next/navigation";

import type { Metadata } from "next";

export const metadata: Metadata = {
	title: "Drucker reservieren",
};

interface PrinterReservePageProps {
	params: {
		printerId: string;
	};
}

export default async function PrinterReservePage(props: PrinterReservePageProps) {
	const { user } = await validateRequest();
	const { printerId } = props.params;

	if (!user) {
		return redirect("/");
	}

	return (
		<Card>
			<CardHeader>
				<CardTitle>Drucker reservieren</CardTitle>
			</CardHeader>
			<CardContent>
				<PrinterReserveForm userId={user?.id} printerId={printerId} />
			</CardContent>
		</Card>
	);
}

================
File: src/components/data-card.tsx
================
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { icons } from "lucide-react";

interface GenericIconProps {
	name: keyof typeof icons;
	className: string;
}

function GenericIcon(props: GenericIconProps) {
	const { name, className } = props;
	const LucideIcon = icons[name];

	return <LucideIcon className={className} />;
}

interface DataCardProps {
	title: string;
	description?: string;
	value: string | number;
	icon: keyof typeof icons;
}

export function DataCard(props: DataCardProps) {
	const { title, description, value, icon } = props;

	return (
		<Card className="w-full">
			<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
				<CardTitle className="text-sm font-medium">{title}</CardTitle>
				<GenericIcon name={icon} className="h-4 w-4 text-muted-foreground" />
			</CardHeader>
			<CardContent>
				<div className="text-2xl font-bold">{value}</div>
				<p className="text-xs text-muted-foreground">&nbsp;</p>
			</CardContent>
		</Card>
	);
}

================
File: src/components/dynamic-printer-cards.tsx
================
"use client";

import { PrinterCard } from "@/components/printer-card";
import { Skeleton } from "@/components/ui/skeleton";
import type { InferResultType } from "@/utils/drizzle";
import { fetcher } from "@/utils/fetch";
import type { RegisteredDatabaseUserAttributes } from "lucia";
import useSWR from "swr";

interface DynamicPrinterCardsProps {
	user: RegisteredDatabaseUserAttributes | null;
}

export function DynamicPrinterCards(props: DynamicPrinterCardsProps) {
	const { user } = props;
	const { data, error, isLoading } = useSWR("/api/printers", fetcher, {
		refreshInterval: 1000 * 15,
	});

	if (error) {
		return <div>Ein Fehler ist aufgetreten.</div>;
	}

	if (isLoading) {
		return (
			<>
				{new Array(6).fill(null).map((_, index) => (
					// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
					<Skeleton key={index} className="w-auto h-36 animate-pulse" />
				))}
			</>
		);
	}

	return data.map((printer: InferResultType<"printers", { printJobs: true }>) => {
		return <PrinterCard key={printer.id} printer={printer} user={user} />;
	});
}

================
File: src/components/header/index.tsx
================
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 {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuGroup,
	DropdownMenuItem,
	DropdownMenuLabel,
	DropdownMenuSeparator,
	DropdownMenuTrigger,
} 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 Link from "next/link";
import { If, Then } from "react-if";

function getInitials(name: string | undefined) {
	if (!name) return "";

	const parts = name.split(" ");
	if (parts.length === 1) return parts[0].slice(0, 2);

	return parts[0].charAt(0) + parts[parts.length - 1].charAt(0);
}

export async function Header() {
	const { user } = await validateRequest();

	return (
		<header className="h-16 bg-neutral-900 border-b-4 border-neutral-600 text-white select-none shadow-md">
			<div className="px-8 h-full max-w-screen-2xl w-full mx-auto flex items-center justify-between">
				<div className="flex flex-row items-center gap-8">
					<Link href="/" className="flex items-center gap-2">
						<StickerIcon size={20} />
						<h1 className="text-lg font-mono">MYP</h1>
					</Link>
					<HeaderNavigation />
				</div>

				{user != null && (
					<DropdownMenu>
						<DropdownMenuTrigger>
							<Avatar>
								<AvatarFallback className="bg-neutral-700">
									<span className="font-semibold">{getInitials(user?.displayName)}</span>
								</AvatarFallback>
							</Avatar>
						</DropdownMenuTrigger>
						<DropdownMenuContent align="end" className="w-56">
							<DropdownMenuGroup>
								<DropdownMenuLabel>Mein Account</DropdownMenuLabel>
								<DropdownMenuSeparator />
								<DropdownMenuItem asChild>
									<Link href="/my/profile/" className="flex items-center gap-2">
										<UserIcon className="w-4 h-4" />
										<span>Mein Profil</span>
									</Link>
								</DropdownMenuItem>

								<If condition={hasRole(user, UserRole.ADMIN)}>
									<Then>
										<DropdownMenuItem asChild>
											<Link href="/admin/" className="flex items-center gap-2">
												<WrenchIcon className="w-4 h-4" />
												<span>Adminbereich</span>
											</Link>
										</DropdownMenuItem>
									</Then>
								</If>
								<DropdownMenuSeparator />
								<DropdownMenuItem>
									<LogoutButton />
								</DropdownMenuItem>
							</DropdownMenuGroup>
						</DropdownMenuContent>
					</DropdownMenu>
				)}
				{user == null && <LoginButton />}
			</div>
		</header>
	);
}

================
File: src/components/header/navigation.tsx
================
"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;
}

export function HeaderNavigation() {
	const pathname = usePathname();
	const sites: Site[] = [
		{
			name: "Dashboard",
			icon: <LayersIcon className="w-4 h-4" />,
			path: "/",
		},
		/* {
			name: "Meine Druckaufträge",
			path: "/my/jobs",
		}, */
		{
			name: "Mein Profil",
			icon: <ContactRoundIcon className="w-4 h-4" />,
			path: "/my/profile",
		},
	];

	return (
		<nav className="font-medium text-sm flex items-center gap-4 flex-row">
			{sites.map((site) => (
				<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,
						"text-neutral-500": pathname !== site.path,
					})}
				>
					{site.icon}
					<span>{site.name}</span>
				</Link>
			))}
		</nav>
	);
}

================
File: src/components/login-button.tsx
================
"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>
	);
}

================
File: src/components/logout-button.tsx
================
"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">
			<LogOutIcon className="w-4 h-4" />
			<span>Abmelden</span>
		</Link>
	);
}

================
File: src/components/personalized-cards.tsx
================
import { DataCard } from "@/components/data-card";
import { validateRequest } from "@/server/auth";
import { db } from "@/server/db";
import { eq } from "drizzle-orm";

export default async function PersonalizedCards() {
	const { user } = await validateRequest();

	if (!user) {
		return null;
	}

	const allPrintJobs = await db.query.printJobs.findMany({
		with: {
			printer: true,
		},
		where: (printJobs) => eq(printJobs.userId, user.id),
	});

	const totalPrintingMinutes = allPrintJobs
		.filter((job) => !job.aborted)
		.reduce((acc, curr) => acc + curr.durationInMinutes, 0);
	const averagePrintingHoursPerWeek = totalPrintingMinutes / 60 / 52;

	const mostUsedPrinters = {printer:{name:'-'}}; /*allPrintJobs
		.map((job) => job.printer.name)
		.reduce((acc, curr) => {
			acc[curr] = (acc[curr] || 0) + 1;
			return acc;
		}, {});*/

	const mostUsedPrinter = 0; /*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
		.map((job) => job.startAt.getDay())
		.reduce((acc, curr) => {
			acc[curr] = (acc[curr] || 0) + 1;
			return acc;
		}, {});*/

	const mostUsedWeekdayIndex = ""; /*Object.keys(mostUsedWeekday).reduce((a, b) =>
		mostUsedWeekday[a] > mostUsedWeekday[b] ? a : b,
	);*/

	const mostUsedWeekdayName = new Intl.DateTimeFormat("de-DE", {
		weekday: "long",
	}).format(new Date(0, 0, Number.parseInt(mostUsedWeekdayIndex)));

	return (
		<div className="flex flex-col lg:flex-row gap-4">
			<DataCard
				icon="Clock10"
				title="Druckstunden"
				description="insgesamt"
				value={`${(totalPrintingMinutes / 60).toFixed(2)}h`}
			/>
			<DataCard
				icon="Calendar"
				title="Aktivster Tag"
				description="(nach Anzahl der Aufträgen)"
				value={mostUsedWeekdayName}
			/>
			<DataCard icon="Heart" title="Lieblingsdrucker" description="" value={mostUsedPrinter} />
			<DataCard icon="Check" title="Druckerfolgsquote" description="" value={`${printerSuccessRate.toFixed(2)}%`} />
		</div>
	);
}

================
File: src/components/printer-availability-badge.tsx
================
import { Badge } from "@/components/ui/badge";
import { PrinterStatus, translatePrinterStatus } from "@/utils/printers";
import { cn } from "@/utils/styles";

interface PrinterAvailabilityBadgeProps {
	status: PrinterStatus;
}

export function PrinterAvailabilityBadge(props: PrinterAvailabilityBadgeProps) {
	const { status } = props;

	return (
		<Badge
			className={cn("pointer-events-none select-none", {
				"bg-green-500 hover:bg-green-500 animate-pulse":
					status === PrinterStatus.IDLE,
				"bg-red-500 hover:bg-red-500 opacity-50":
					status === PrinterStatus.OUT_OF_ORDER,
				"bg-orange-500 hover:bg-orange-500": status === PrinterStatus.RESERVED,
			})}
		>
			{translatePrinterStatus(status)}
		</Badge>
	);
}

================
File: src/components/printer-card/countdown.tsx
================
"use client";
import { revalidate } from "@/server/actions/timer";
import { fetcher } from "@/utils/fetch";
import useSWR from "swr";

interface CountdownProps {
	jobId: string;
}
export function Countdown(props: CountdownProps) {
	const { jobId } = props;
	const { data, error, isLoading } = useSWR(`/api/job/${jobId}/remaining-time`, fetcher, {
		refreshInterval: 1000 * 30,
	});

	if (error) {
		return <span className="text-red-500">Ein Fehler ist aufgetreten.</span>;
	}

	if (isLoading) {
		return <>...</>;
	}

	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));
	const seconds = Math.floor((data.remainingTime % (1000 * 60)) / 1000);

	if (days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0) {
		revalidate();
	}

	return (
		<span className="tabular-nums" suppressHydrationWarning>
			{days > 0 && <>{`${days}`.padStart(2, "0")}d </>}
			{hours === 0 && minutes === 0 ? (
				<>{`${seconds}`.padStart(2, "0")}s</>
			) : (
				<>
					{`${hours}`.padStart(2, "0")}h {`${minutes}`.padStart(2, "0")}min
				</>
			)}
		</span>
	);
}

================
File: src/components/printer-card/index.tsx
================
"use client";

import { PrinterReserveForm } from "@/app/printer/[printerId]/reserve/form";
import { Countdown } from "@/components/printer-card/countdown";
import { AlertDialogHeader } from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { UserRole, hasRole } from "@/server/auth/permissions";
import type { InferResultType } from "@/utils/drizzle";
import { PrinterStatus, derivePrinterStatus, translatePrinterStatus } from "@/utils/printers";
import { cn } from "@/utils/styles";
import type { RegisteredDatabaseUserAttributes } from "lucia";
import { CalendarPlusIcon, ChevronRightIcon } from "lucide-react";
import Link from "next/link";
import { Else, If, Then } from "react-if";

interface PrinterCardProps {
	printer: InferResultType<"printers", { printJobs: true }>;
	user?: RegisteredDatabaseUserAttributes | null;
}

export function PrinterCard(props: PrinterCardProps) {
	const { printer, user } = props;
	const status = derivePrinterStatus(printer);

	const userIsLoggedIn = Boolean(user);

	return (
		<Card
			className={cn("w-auto h-36", {
				"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>
				</div>
			</CardHeader>
			<CardContent className="flex justify-end">
				<If condition={status === PrinterStatus.IDLE && userIsLoggedIn && !hasRole(user, UserRole.GUEST)}>
					<Then>
						<Dialog>
							<DialogTrigger asChild>
								<Button variant={"default"} className="flex items-center gap-2 w-full">
									<CalendarPlusIcon className="w-4 h-4" />
									<span>Reservieren</span>
								</Button>
							</DialogTrigger>
							<DialogContent>
								<AlertDialogHeader>
									<DialogTitle>{printer.name} reservieren</DialogTitle>
									<DialogDescription>Gebe die geschätzte Druckdauer an.</DialogDescription>
								</AlertDialogHeader>
								<PrinterReserveForm isDialog={true} printerId={printer.id} userId={user?.id ?? ""} />
							</DialogContent>
						</Dialog>
					</Then>
				</If>
				{status === PrinterStatus.RESERVED && (
					<Button asChild variant={"secondary"}>
						<Link href={`/job/${printer.printJobs[0].id}`} className="flex items-center gap-2 w-full">
							<ChevronRightIcon className="w-4 h-4" />
							<span>Details anzeigen</span>
						</Link>
					</Button>
				)}
			</CardContent>
		</Card>
	);
}

================
File: src/components/ui/alert-dialog.tsx
================
"use client"

import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"

import { cn } from "@/utils/styles"
import { buttonVariants } from "@/components/ui/button"

const AlertDialog = AlertDialogPrimitive.Root

const AlertDialogTrigger = AlertDialogPrimitive.Trigger

const AlertDialogPortal = AlertDialogPrimitive.Portal

const AlertDialogOverlay = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Overlay
    className={cn(
      "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className
    )}
    {...props}
    ref={ref}
  />
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName

const AlertDialogContent = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
  <AlertDialogPortal>
    <AlertDialogOverlay />
    <AlertDialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
        className
      )}
      {...props}
    />
  </AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName

const AlertDialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 text-center sm:text-left",
      className
    )}
    {...props}
  />
)
AlertDialogHeader.displayName = "AlertDialogHeader"

const AlertDialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
)
AlertDialogFooter.displayName = "AlertDialogFooter"

const AlertDialogTitle = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold", className)}
    {...props}
  />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName

const AlertDialogDescription = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
AlertDialogDescription.displayName =
  AlertDialogPrimitive.Description.displayName

const AlertDialogAction = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Action
    ref={ref}
    className={cn(buttonVariants(), className)}
    {...props}
  />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName

const AlertDialogCancel = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Cancel
    ref={ref}
    className={cn(
      buttonVariants({ variant: "outline" }),
      "mt-2 sm:mt-0",
      className
    )}
    {...props}
  />
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName

export {
  AlertDialog,
  AlertDialogPortal,
  AlertDialogOverlay,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogAction,
  AlertDialogCancel,
}

================
File: src/components/ui/alert.tsx
================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/utils/styles"

const alertVariants = cva(
  "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
  {
    variants: {
      variant: {
        default: "bg-background text-foreground",
        destructive:
          "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

const Alert = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
  <div
    ref={ref}
    role="alert"
    className={cn(alertVariants({ variant }), className)}
    {...props}
  />
))
Alert.displayName = "Alert"

const AlertTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h5
    ref={ref}
    className={cn("mb-1 font-medium leading-none tracking-tight", className)}
    {...props}
  />
))
AlertTitle.displayName = "AlertTitle"

const AlertDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("text-sm [&_p]:leading-relaxed", className)}
    {...props}
  />
))
AlertDescription.displayName = "AlertDescription"

export { Alert, AlertTitle, AlertDescription }

================
File: src/components/ui/avatar.tsx
================
"use client"

import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"

import { cn } from "@/utils/styles"

const Avatar = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Root
    ref={ref}
    className={cn(
      "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
      className
    )}
    {...props}
  />
))
Avatar.displayName = AvatarPrimitive.Root.displayName

const AvatarImage = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Image>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Image
    ref={ref}
    className={cn("aspect-square h-full w-full", className)}
    {...props}
  />
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName

const AvatarFallback = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Fallback>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Fallback
    ref={ref}
    className={cn(
      "flex h-full w-full items-center justify-center rounded-full bg-muted",
      className
    )}
    {...props}
  />
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName

export { Avatar, AvatarImage, AvatarFallback }

================
File: src/components/ui/badge.tsx
================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/utils/styles"

const badgeVariants = cva(
  "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
  {
    variants: {
      variant: {
        default:
          "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
        secondary:
          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
        destructive:
          "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
        outline: "text-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
  )
}

export { Badge, badgeVariants }

================
File: src/components/ui/breadcrumb.tsx
================
import * as React from "react"
import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
import { Slot } from "@radix-ui/react-slot"

import { cn } from "@/utils/styles"

const Breadcrumb = React.forwardRef<
  HTMLElement,
  React.ComponentPropsWithoutRef<"nav"> & {
    separator?: React.ReactNode
  }
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"

const BreadcrumbList = React.forwardRef<
  HTMLOListElement,
  React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
  <ol
    ref={ref}
    className={cn(
      "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
      className
    )}
    {...props}
  />
))
BreadcrumbList.displayName = "BreadcrumbList"

const BreadcrumbItem = React.forwardRef<
  HTMLLIElement,
  React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
  <li
    ref={ref}
    className={cn("inline-flex items-center gap-1.5", className)}
    {...props}
  />
))
BreadcrumbItem.displayName = "BreadcrumbItem"

const BreadcrumbLink = React.forwardRef<
  HTMLAnchorElement,
  React.ComponentPropsWithoutRef<"a"> & {
    asChild?: boolean
  }
>(({ asChild, className, ...props }, ref) => {
  const Comp = asChild ? Slot : "a"

  return (
    <Comp
      ref={ref}
      className={cn("transition-colors hover:text-foreground", className)}
      {...props}
    />
  )
})
BreadcrumbLink.displayName = "BreadcrumbLink"

const BreadcrumbPage = React.forwardRef<
  HTMLSpanElement,
  React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    role="link"
    aria-disabled="true"
    aria-current="page"
    className={cn("font-normal text-foreground", className)}
    {...props}
  />
))
BreadcrumbPage.displayName = "BreadcrumbPage"

const BreadcrumbSeparator = ({
  children,
  className,
  ...props
}: React.ComponentProps<"li">) => (
  <li
    role="presentation"
    aria-hidden="true"
    className={cn("[&>svg]:size-3.5", className)}
    {...props}
  >
    {children ?? <ChevronRightIcon />}
  </li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"

const BreadcrumbEllipsis = ({
  className,
  ...props
}: React.ComponentProps<"span">) => (
  <span
    role="presentation"
    aria-hidden="true"
    className={cn("flex h-9 w-9 items-center justify-center", className)}
    {...props}
  >
    <DotsHorizontalIcon className="h-4 w-4" />
    <span className="sr-only">More</span>
  </span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"

export {
  Breadcrumb,
  BreadcrumbList,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbPage,
  BreadcrumbSeparator,
  BreadcrumbEllipsis,
}

================
File: src/components/ui/button.tsx
================
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/utils/styles"

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default:
          "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline:
          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }

================
File: src/components/ui/card.tsx
================
import * as React from "react"

import { cn } from "@/utils/styles"

const Card = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "rounded-xl border bg-card text-card-foreground shadow",
      className
    )}
    {...props}
  />
))
Card.displayName = "Card"

const CardHeader = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex flex-col space-y-1.5 p-6", className)}
    {...props}
  />
))
CardHeader.displayName = "CardHeader"

const CardTitle = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
  <h3
    ref={ref}
    className={cn("font-semibold leading-none tracking-tight", className)}
    {...props}
  />
))
CardTitle.displayName = "CardTitle"

const CardDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
CardDescription.displayName = "CardDescription"

const CardContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"

const CardFooter = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex items-center p-6 pt-0", className)}
    {...props}
  />
))
CardFooter.displayName = "CardFooter"

export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

================
File: src/components/ui/chart.tsx
================
"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,
}

================
File: src/components/ui/dialog.tsx
================
"use client"

import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Cross2Icon } from "@radix-ui/react-icons"

import { cn } from "@/utils/styles"

const Dialog = DialogPrimitive.Root

const DialogTrigger = DialogPrimitive.Trigger

const DialogPortal = DialogPrimitive.Portal

const DialogClose = DialogPrimitive.Close

const DialogOverlay = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Overlay
    ref={ref}
    className={cn(
      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className
    )}
    {...props}
  />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay />
    <DialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
        className
      )}
      {...props}
    >
      {children}
      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
        <Cross2Icon className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </DialogPrimitive.Close>
    </DialogPrimitive.Content>
  </DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

const DialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-1.5 text-center sm:text-left",
      className
    )}
    {...props}
  />
)
DialogHeader.displayName = "DialogHeader"

const DialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
)
DialogFooter.displayName = "DialogFooter"

const DialogTitle = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Title
    ref={ref}
    className={cn(
      "text-lg font-semibold leading-none tracking-tight",
      className
    )}
    {...props}
  />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName

const DialogDescription = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <DialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName

export {
  Dialog,
  DialogPortal,
  DialogOverlay,
  DialogTrigger,
  DialogClose,
  DialogContent,
  DialogHeader,
  DialogFooter,
  DialogTitle,
  DialogDescription,
}

================
File: src/components/ui/dropdown-menu.tsx
================
"use client"

import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
  CheckIcon,
  ChevronRightIcon,
  DotFilledIcon,
} from "@radix-ui/react-icons"

import { cn } from "@/utils/styles"

const DropdownMenu = DropdownMenuPrimitive.Root

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger

const DropdownMenuGroup = DropdownMenuPrimitive.Group

const DropdownMenuPortal = DropdownMenuPrimitive.Portal

const DropdownMenuSub = DropdownMenuPrimitive.Sub

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup

const DropdownMenuSubTrigger = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
    inset?: boolean
  }
>(({ className, inset, children, ...props }, ref) => (
  <DropdownMenuPrimitive.SubTrigger
    ref={ref}
    className={cn(
      "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
      inset && "pl-8",
      className
    )}
    {...props}
  >
    {children}
    <ChevronRightIcon className="ml-auto h-4 w-4" />
  </DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
  DropdownMenuPrimitive.SubTrigger.displayName

const DropdownMenuSubContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.SubContent
    ref={ref}
    className={cn(
      "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
      className
    )}
    {...props}
  />
))
DropdownMenuSubContent.displayName =
  DropdownMenuPrimitive.SubContent.displayName

const DropdownMenuContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
  <DropdownMenuPrimitive.Portal>
    <DropdownMenuPrimitive.Content
      ref={ref}
      sideOffset={sideOffset}
      className={cn(
        "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        className
      )}
      {...props}
    />
  </DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName

const DropdownMenuItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
    inset?: boolean
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Item
    ref={ref}
    className={cn(
      "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      inset && "pl-8",
      className
    )}
    {...props}
  />
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName

const DropdownMenuCheckboxItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
  <DropdownMenuPrimitive.CheckboxItem
    ref={ref}
    className={cn(
      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    checked={checked}
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <CheckIcon className="h-4 w-4" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
  DropdownMenuPrimitive.CheckboxItem.displayName

const DropdownMenuRadioItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
  <DropdownMenuPrimitive.RadioItem
    ref={ref}
    className={cn(
      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <DotFilledIcon className="h-4 w-4 fill-current" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName

const DropdownMenuLabel = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
    inset?: boolean
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Label
    ref={ref}
    className={cn(
      "px-2 py-1.5 text-sm font-semibold",
      inset && "pl-8",
      className
    )}
    {...props}
  />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName

const DropdownMenuSeparator = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName

const DropdownMenuShortcut = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
  return (
    <span
      className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
      {...props}
    />
  )
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"

export {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuGroup,
  DropdownMenuPortal,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuRadioGroup,
}

================
File: src/components/ui/form.tsx
================
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
  Controller,
  ControllerProps,
  FieldPath,
  FieldValues,
  FormProvider,
  useFormContext,
} from "react-hook-form"

import { cn } from "@/utils/styles"
import { Label } from "@/components/ui/label"

const Form = FormProvider

type FormFieldContextValue<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
  name: TName
}

const FormFieldContext = React.createContext<FormFieldContextValue>(
  {} as FormFieldContextValue
)

const FormField = <
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
  ...props
}: ControllerProps<TFieldValues, TName>) => {
  return (
    <FormFieldContext.Provider value={{ name: props.name }}>
      <Controller {...props} />
    </FormFieldContext.Provider>
  )
}

const useFormField = () => {
  const fieldContext = React.useContext(FormFieldContext)
  const itemContext = React.useContext(FormItemContext)
  const { getFieldState, formState } = useFormContext()

  const fieldState = getFieldState(fieldContext.name, formState)

  if (!fieldContext) {
    throw new Error("useFormField should be used within <FormField>")
  }

  const { id } = itemContext

  return {
    id,
    name: fieldContext.name,
    formItemId: `${id}-form-item`,
    formDescriptionId: `${id}-form-item-description`,
    formMessageId: `${id}-form-item-message`,
    ...fieldState,
  }
}

type FormItemContextValue = {
  id: string
}

const FormItemContext = React.createContext<FormItemContextValue>(
  {} as FormItemContextValue
)

const FormItem = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
  const id = React.useId()

  return (
    <FormItemContext.Provider value={{ id }}>
      <div ref={ref} className={cn("space-y-2", className)} {...props} />
    </FormItemContext.Provider>
  )
})
FormItem.displayName = "FormItem"

const FormLabel = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
  const { error, formItemId } = useFormField()

  return (
    <Label
      ref={ref}
      className={cn(error && "text-destructive", className)}
      htmlFor={formItemId}
      {...props}
    />
  )
})
FormLabel.displayName = "FormLabel"

const FormControl = React.forwardRef<
  React.ElementRef<typeof Slot>,
  React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()

  return (
    <Slot
      ref={ref}
      id={formItemId}
      aria-describedby={
        !error
          ? `${formDescriptionId}`
          : `${formDescriptionId} ${formMessageId}`
      }
      aria-invalid={!!error}
      {...props}
    />
  )
})
FormControl.displayName = "FormControl"

const FormDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
  const { formDescriptionId } = useFormField()

  return (
    <p
      ref={ref}
      id={formDescriptionId}
      className={cn("text-[0.8rem] text-muted-foreground", className)}
      {...props}
    />
  )
})
FormDescription.displayName = "FormDescription"

const FormMessage = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
  const { error, formMessageId } = useFormField()
  const body = error ? String(error?.message) : children

  if (!body) {
    return null
  }

  return (
    <p
      ref={ref}
      id={formMessageId}
      className={cn("text-[0.8rem] font-medium text-destructive", className)}
      {...props}
    >
      {body}
    </p>
  )
})
FormMessage.displayName = "FormMessage"

export {
  useFormField,
  Form,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
  FormField,
}

================
File: src/components/ui/hover-card.tsx
================
"use client"

import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"

import { cn } from "@/utils/styles"

const HoverCard = HoverCardPrimitive.Root

const HoverCardTrigger = HoverCardPrimitive.Trigger

const HoverCardContent = React.forwardRef<
  React.ElementRef<typeof HoverCardPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
  <HoverCardPrimitive.Content
    ref={ref}
    align={align}
    sideOffset={sideOffset}
    className={cn(
      "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
      className
    )}
    {...props}
  />
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName

export { HoverCard, HoverCardTrigger, HoverCardContent }

================
File: src/components/ui/input.tsx
================
import * as React from "react"

import { cn } from "@/utils/styles"

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"

export { Input }

================
File: src/components/ui/label.tsx
================
"use client"

import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/utils/styles"

const labelVariants = cva(
  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)

const Label = React.forwardRef<
  React.ElementRef<typeof LabelPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
    VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
  <LabelPrimitive.Root
    ref={ref}
    className={cn(labelVariants(), className)}
    {...props}
  />
))
Label.displayName = LabelPrimitive.Root.displayName

export { Label }

================
File: src/components/ui/scroll-area.tsx
================
"use client"

import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

import { cn } from "@/utils/styles"

const ScrollArea = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
  <ScrollAreaPrimitive.Root
    ref={ref}
    className={cn("relative overflow-hidden", className)}
    {...props}
  >
    <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
      {children}
    </ScrollAreaPrimitive.Viewport>
    <ScrollBar />
    <ScrollAreaPrimitive.Corner />
  </ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
  <ScrollAreaPrimitive.ScrollAreaScrollbar
    ref={ref}
    orientation={orientation}
    className={cn(
      "flex touch-none select-none transition-colors",
      orientation === "vertical" &&
        "h-full w-2.5 border-l border-l-transparent p-[1px]",
      orientation === "horizontal" &&
        "h-2.5 flex-col border-t border-t-transparent p-[1px]",
      className
    )}
    {...props}
  >
    <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
  </ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }

================
File: src/components/ui/select.tsx
================
"use client"

import * as React from "react"
import {
  CaretSortIcon,
  CheckIcon,
  ChevronDownIcon,
  ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"

import { cn } from "@/utils/styles"

const Select = SelectPrimitive.Root

const SelectGroup = SelectPrimitive.Group

const SelectValue = SelectPrimitive.Value

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Trigger
    ref={ref}
    className={cn(
      "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
      className
    )}
    {...props}
  >
    {children}
    <SelectPrimitive.Icon asChild>
      <CaretSortIcon className="h-4 w-4 opacity-50" />
    </SelectPrimitive.Icon>
  </SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName

const SelectScrollUpButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollUpButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronUpIcon />
  </SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName

const SelectScrollDownButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollDownButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronDownIcon />
  </SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
  SelectPrimitive.ScrollDownButton.displayName

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(
        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        position === "popper" &&
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        className
      )}
      position={position}
      {...props}
    >
      <SelectScrollUpButton />
      <SelectPrimitive.Viewport
        className={cn(
          "p-1",
          position === "popper" &&
            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
        )}
      >
        {children}
      </SelectPrimitive.Viewport>
      <SelectScrollDownButton />
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Label
    ref={ref}
    className={cn("px-2 py-1.5 text-sm font-semibold", className)}
    {...props}
  />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className={cn(
      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    {...props}
  >
    <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <CheckIcon className="h-4 w-4" />
      </SelectPrimitive.ItemIndicator>
    </span>
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

export {
  Select,
  SelectGroup,
  SelectValue,
  SelectTrigger,
  SelectContent,
  SelectLabel,
  SelectItem,
  SelectSeparator,
  SelectScrollUpButton,
  SelectScrollDownButton,
}

================
File: src/components/ui/skeleton.tsx
================
import { cn } from "@/utils/styles"

function Skeleton({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("animate-pulse rounded-md bg-primary/10", className)}
      {...props}
    />
  )
}

export { Skeleton }

================
File: src/components/ui/sonner.tsx
================
"use client"

import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme()

  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      className="toaster group"
      toastOptions={{
        classNames: {
          toast:
            "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
          description: "group-[.toast]:text-muted-foreground",
          actionButton:
            "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
          cancelButton:
            "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
        },
      }}
      {...props}
    />
  )
}

export { Toaster }

================
File: src/components/ui/table.tsx
================
import * as React from "react"

import { cn } from "@/utils/styles"

const Table = React.forwardRef<
  HTMLTableElement,
  React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
  <div className="relative w-full overflow-auto">
    <table
      ref={ref}
      className={cn("w-full caption-bottom text-sm", className)}
      {...props}
    />
  </div>
))
Table.displayName = "Table"

const TableHeader = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"

const TableBody = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <tbody
    ref={ref}
    className={cn("[&_tr:last-child]:border-0", className)}
    {...props}
  />
))
TableBody.displayName = "TableBody"

const TableFooter = React.forwardRef<
  HTMLTableSectionElement,
  React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
  <tfoot
    ref={ref}
    className={cn(
      "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
      className
    )}
    {...props}
  />
))
TableFooter.displayName = "TableFooter"

const TableRow = React.forwardRef<
  HTMLTableRowElement,
  React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
  <tr
    ref={ref}
    className={cn(
      "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
      className
    )}
    {...props}
  />
))
TableRow.displayName = "TableRow"

const TableHead = React.forwardRef<
  HTMLTableCellElement,
  React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
  <th
    ref={ref}
    className={cn(
      "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
      className
    )}
    {...props}
  />
))
TableHead.displayName = "TableHead"

const TableCell = React.forwardRef<
  HTMLTableCellElement,
  React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
  <td
    ref={ref}
    className={cn(
      "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
      className
    )}
    {...props}
  />
))
TableCell.displayName = "TableCell"

const TableCaption = React.forwardRef<
  HTMLTableCaptionElement,
  React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
  <caption
    ref={ref}
    className={cn("mt-4 text-sm text-muted-foreground", className)}
    {...props}
  />
))
TableCaption.displayName = "TableCaption"

export {
  Table,
  TableHeader,
  TableBody,
  TableFooter,
  TableHead,
  TableRow,
  TableCell,
  TableCaption,
}

================
File: src/components/ui/tabs.tsx
================
"use client"

import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/utils/styles"

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.List
    ref={ref}
    className={cn(
      "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
      className
    )}
    {...props}
  />
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Trigger
    ref={ref}
    className={cn(
      "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
      className
    )}
    {...props}
  />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Content
    ref={ref}
    className={cn(
      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
      className
    )}
    {...props}
  />
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }

================
File: src/components/ui/textarea.tsx
================
import * as React from "react"

import { cn } from "@/utils/styles"

export interface TextareaProps
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
  ({ className, ...props }, ref) => {
    return (
      <textarea
        className={cn(
          "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Textarea.displayName = "Textarea"

export { Textarea }

================
File: src/components/ui/toast.tsx
================
"use client"

import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/utils/styles"

const ToastProvider = ToastPrimitives.Provider

const ToastViewport = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Viewport>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Viewport
    ref={ref}
    className={cn(
      "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
      className
    )}
    {...props}
  />
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName

const toastVariants = cva(
  "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
  {
    variants: {
      variant: {
        default: "border bg-background text-foreground",
        destructive:
          "destructive group border-destructive bg-destructive text-destructive-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

const Toast = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
    VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
  return (
    <ToastPrimitives.Root
      ref={ref}
      className={cn(toastVariants({ variant }), className)}
      {...props}
    />
  )
})
Toast.displayName = ToastPrimitives.Root.displayName

const ToastAction = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Action>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Action
    ref={ref}
    className={cn(
      "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
      className
    )}
    {...props}
  />
))
ToastAction.displayName = ToastPrimitives.Action.displayName

const ToastClose = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Close>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Close
    ref={ref}
    className={cn(
      "absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
      className
    )}
    toast-close=""
    {...props}
  >
    <Cross2Icon className="h-4 w-4" />
  </ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName

const ToastTitle = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Title>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Title
    ref={ref}
    className={cn("text-sm font-semibold [&+div]:text-xs", className)}
    {...props}
  />
))
ToastTitle.displayName = ToastPrimitives.Title.displayName

const ToastDescription = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Description>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Description
    ref={ref}
    className={cn("text-sm opacity-90", className)}
    {...props}
  />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName

type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>

type ToastActionElement = React.ReactElement<typeof ToastAction>

export {
  type ToastProps,
  type ToastActionElement,
  ToastProvider,
  ToastViewport,
  Toast,
  ToastTitle,
  ToastDescription,
  ToastClose,
  ToastAction,
}

================
File: src/components/ui/toaster.tsx
================
"use client"

import {
  Toast,
  ToastClose,
  ToastDescription,
  ToastProvider,
  ToastTitle,
  ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"

export function Toaster() {
  const { toasts } = useToast()

  return (
    <ToastProvider>
      {toasts.map(function ({ id, title, description, action, ...props }) {
        return (
          <Toast key={id} {...props}>
            <div className="grid gap-1">
              {title && <ToastTitle>{title}</ToastTitle>}
              {description && (
                <ToastDescription>{description}</ToastDescription>
              )}
            </div>
            {action}
            <ToastClose />
          </Toast>
        )
      })}
      <ToastViewport />
    </ToastProvider>
  )
}

================
File: src/components/ui/use-toast.ts
================
"use client"

// Inspired by react-hot-toast library
import * as React from "react"

import type {
  ToastActionElement,
  ToastProps,
} from "@/components/ui/toast"

const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000

type ToasterToast = ToastProps & {
  id: string
  title?: React.ReactNode
  description?: React.ReactNode
  action?: ToastActionElement
}

const actionTypes = {
  ADD_TOAST: "ADD_TOAST",
  UPDATE_TOAST: "UPDATE_TOAST",
  DISMISS_TOAST: "DISMISS_TOAST",
  REMOVE_TOAST: "REMOVE_TOAST",
} as const

let count = 0

function genId() {
  count = (count + 1) % Number.MAX_SAFE_INTEGER
  return count.toString()
}

type ActionType = typeof actionTypes

type Action =
  | {
      type: ActionType["ADD_TOAST"]
      toast: ToasterToast
    }
  | {
      type: ActionType["UPDATE_TOAST"]
      toast: Partial<ToasterToast>
    }
  | {
      type: ActionType["DISMISS_TOAST"]
      toastId?: ToasterToast["id"]
    }
  | {
      type: ActionType["REMOVE_TOAST"]
      toastId?: ToasterToast["id"]
    }

interface State {
  toasts: ToasterToast[]
}

const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()

const addToRemoveQueue = (toastId: string) => {
  if (toastTimeouts.has(toastId)) {
    return
  }

  const timeout = setTimeout(() => {
    toastTimeouts.delete(toastId)
    dispatch({
      type: "REMOVE_TOAST",
      toastId: toastId,
    })
  }, TOAST_REMOVE_DELAY)

  toastTimeouts.set(toastId, timeout)
}

export const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "ADD_TOAST":
      return {
        ...state,
        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
      }

    case "UPDATE_TOAST":
      return {
        ...state,
        toasts: state.toasts.map((t) =>
          t.id === action.toast.id ? { ...t, ...action.toast } : t
        ),
      }

    case "DISMISS_TOAST": {
      const { toastId } = action

      // ! Side effects ! - This could be extracted into a dismissToast() action,
      // but I'll keep it here for simplicity
      if (toastId) {
        addToRemoveQueue(toastId)
      } else {
        state.toasts.forEach((toast) => {
          addToRemoveQueue(toast.id)
        })
      }

      return {
        ...state,
        toasts: state.toasts.map((t) =>
          t.id === toastId || toastId === undefined
            ? {
                ...t,
                open: false,
              }
            : t
        ),
      }
    }
    case "REMOVE_TOAST":
      if (action.toastId === undefined) {
        return {
          ...state,
          toasts: [],
        }
      }
      return {
        ...state,
        toasts: state.toasts.filter((t) => t.id !== action.toastId),
      }
  }
}

const listeners: Array<(state: State) => void> = []

let memoryState: State = { toasts: [] }

function dispatch(action: Action) {
  memoryState = reducer(memoryState, action)
  listeners.forEach((listener) => {
    listener(memoryState)
  })
}

type Toast = Omit<ToasterToast, "id">

function toast({ ...props }: Toast) {
  const id = genId()

  const update = (props: ToasterToast) =>
    dispatch({
      type: "UPDATE_TOAST",
      toast: { ...props, id },
    })
  const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })

  dispatch({
    type: "ADD_TOAST",
    toast: {
      ...props,
      id,
      open: true,
      onOpenChange: (open) => {
        if (!open) dismiss()
      },
    },
  })

  return {
    id: id,
    dismiss,
    update,
  }
}

function useToast() {
  const [state, setState] = React.useState<State>(memoryState)

  React.useEffect(() => {
    listeners.push(setState)
    return () => {
      const index = listeners.indexOf(setState)
      if (index > -1) {
        listeners.splice(index, 1)
      }
    }
  }, [state])

  return {
    ...state,
    toast,
    dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
  }
}

export { useToast, toast }

================
File: src/server/actions/authentication/logout.ts
================
"use server";

import { lucia, validateRequest } from "@/server/auth";
import strings from "@/utils/strings";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";

export async function logout(path?: string) {
	const { session } = await validateRequest();

	if (!session) {
		return {
			error: strings.ERROR.NO_SESSION,
		};
	}

	try {
		await lucia.invalidateSession(session.id);
	} catch (error) {
		return {
			error: strings.ERROR.NO_SESSION,
		};
	}

	const sessionCookie = lucia.createBlankSessionCookie();
	cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);

	revalidatePath(path ?? "/");
}

================
File: src/server/actions/printers.ts
================
"use server";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { printers, users } from "@/server/db/schema";
import { IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { type InferInsertModel, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function createPrinter(printer: InferInsertModel<typeof printers>) {
	const { user } = await validateRequest();

	if (guard(user, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	if (!printer) {
		return {
			error: "Druckerdaten sind erforderlich.",
		};
	}

	try {
		await db.insert(printers).values(printer);
	} catch (error) {
		return {
			error: "Drucker konnte nicht hinzugefügt werden.",
		};
	}

	revalidatePath("/");
}

export async function updatePrinter(id: string, data: InferInsertModel<typeof printers>) {
	const { user } = await validateRequest();

	if (guard(user, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	if (!data) {
		return {
			error: "Druckerdaten sind erforderlich.",
		};
	}

	try {
		await db.update(printers).set(data).where(eq(printers.id, id));
	} catch (error) {
		return {
			error: "Druckerdaten sind erforderlich.",
		};
	}

	revalidatePath("/");
}

export async function deletePrinter(id: string) {
	const { user } = await validateRequest();

	if (guard(user, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	try {
		await db.delete(printers).where(eq(printers.id, id));
	} catch (error) {
		if (error instanceof Error) {
			return {
				error: error.message,
			};
		}
		return {
			error: "Ein unbekannter Fehler ist aufgetreten.",
		};
	}

	revalidatePath("/");
}

export async function getPrinters() {
	return await db.query.printers.findMany({
		with: {
			printJobs: {
				limit: 1,
				orderBy: (printJobs, { desc }) => [desc(printJobs.startAt)],
			},
		},
	});
}

================
File: src/server/actions/printJobs.ts
================
"use server";

import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { printJobs, users } from "@/server/db/schema";
import { IS, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { type InferInsertModel, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function createPrintJob(printJob: InferInsertModel<typeof printJobs>) {
	const { user } = await validateRequest();

	if (guard(user, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	try {
		const result = await db.insert(printJobs).values(printJob).returning({
			jobId: printJobs.id,
		});

		return result[0].jobId;
	} catch (error) {
		return {
			error: "Druckauftrag konnte nicht hinzugefügt werden.",
		};
	}
}

/* async function updatePrintJob(jobId: string, printJob: InferInsertModel<typeof printJobs>) {
	const { user } = await validateRequest();

	if (guard(user, is, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION
		}
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, is, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION
		}
	}

	await db.update(printJobs).set(printJob).where(eq(printJobs.id, jobId));
} */

export async function abortPrintJob(jobId: string, reason: string) {
	const { user } = await validateRequest();

	if (guard(user, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	// Get the print job
	const printJob = await db.query.printJobs.findFirst({
		where: eq(printJobs.id, jobId),
	});

	if (!printJob) {
		return {
			error: "Druckauftrag nicht gefunden",
		};
	}

	// Check if the print job is already aborted or completed
	if (printJob.aborted) {
		return { error: "Druckauftrag wurde bereits abgebrochen" };
	}

	if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
		return { error: "Druckauftrag ist bereits abgeschlossen" };
	}

	// Check if user is the owner of the print job
	// biome-ignore lint/style/noNonNullAssertion:  guard already checks against null
	if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	// Get duration in minutes since startAt
	const duration = Math.floor((Date.now() - new Date(printJob.startAt).getTime()) / 1000 / 60);

	await db
		.update(printJobs)
		.set({
			aborted: true,
			abortReason: reason,
			durationInMinutes: duration,
			comments: `${printJob.comments}\n\n---${dbUser?.username}: Druckauftrag abgebrochen`,
		})
		.where(eq(printJobs.id, jobId));

	revalidatePath("/");
}

export async function earlyFinishPrintJob(jobId: string) {
	const { user } = await validateRequest();

	if (guard(user, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	// Get the print job
	const printJob = await db.query.printJobs.findFirst({
		where: eq(printJobs.id, jobId),
	});

	if (!printJob) {
		return { error: "Druckauftrag nicht gefunden" };
	}

	// Check if the print job is already aborted or completed
	if (printJob.aborted) {
		return { error: "Druckauftrag wurde bereits abgebrochen" };
	}

	if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
		return { error: "Druckauftrag ist bereits abgeschlossen" };
	}

	// Check if user is the owner of the print job
	// biome-ignore lint/style/noNonNullAssertion:  guard already checks against null
	if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	// Get duration in minutes since startAt
	const duration = Math.floor((Date.now() - new Date(printJob.startAt).getTime()) / 1000 / 60);

	await db
		.update(printJobs)
		.set({
			durationInMinutes: duration,
			comments: `${printJob.comments}\n\n---${dbUser?.username}: Druckauftrag vorzeitig abgeschlossen`,
		})
		.where(eq(printJobs.id, jobId));

	revalidatePath("/");
}

export async function extendPrintJob(jobId: string, minutes: number, hours: number) {
	const { user } = await validateRequest();

	if (guard(user, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	// Get the print job
	const printJob = await db.query.printJobs.findFirst({
		where: eq(printJobs.id, jobId),
	});

	if (!printJob) {
		return { error: "Druckauftrag nicht gefunden" };
	}

	// Check if the print job is already aborted or completed
	if (printJob.aborted) {
		return { error: "Druckauftrag wurde bereits abgebrochen" };
	}

	if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
		return { error: "Druckauftrag ist bereits abgeschlossen" };
	}

	// Check if user is the owner of the print job
	// biome-ignore lint/style/noNonNullAssertion:  guard already checks against null
	if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const duration = minutes + hours * 60;

	await db
		.update(printJobs)
		.set({
			durationInMinutes: printJob.durationInMinutes + duration,
			comments: `${printJob.comments}\n\n---${dbUser?.username}: Verlängert um ${hours} Stunden und ${minutes} Minuten`,
		})
		.where(eq(printJobs.id, jobId));

	revalidatePath("/");
}

export async function updatePrintComments(jobId: string, comments: string) {
	const { user } = await validateRequest();

	if (guard(user, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS, UserRole.GUEST)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	// Get the print job
	const printJob = await db.query.printJobs.findFirst({
		where: eq(printJobs.id, jobId),
	});

	if (!printJob) {
		return { error: "Druckauftrag nicht gefunden" };
	}

	// Check if the print job is already aborted or completed
	if (printJob.aborted) {
		return { error: "Druckauftrag wurde bereits abgebrochen" };
	}

	if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
		return { error: "Druckauftrag ist bereits abgeschlossen" };
	}

	// Check if user is the owner of the print job
	// biome-ignore lint/style/noNonNullAssertion:  guard already checks against null
	if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	await db
		.update(printJobs)
		.set({
			comments,
		})
		.where(eq(printJobs.id, jobId));

	revalidatePath("/");
}

================
File: src/server/actions/timer.ts
================
"use server";

import { revalidatePath } from "next/cache";

export async function revalidate() {
	revalidatePath("/");
}

================
File: src/server/actions/user/delete.ts
================
"use server";

import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { IS, IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

/**
 * Deletes a user from the database
 * @param userId User ID to delete
 * @param path Path to revalidate
 */
export async function deleteUser(userId: string, path?: string) {
	const { user } = await validateRequest();

	if (guard(user, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const targetUser = await db.query.users.findFirst({
		where: eq(users.id, userId),
	});

	if (!targetUser) {
		return {
			error: "Benutzer nicht gefunden",
		};
	}

	if (guard(targetUser, IS, UserRole.ADMIN)) {
		return {
			error: "Admins können nicht gelöscht werden.",
		};
	}

	await db.delete(users).where(eq(users.id, userId));

	revalidatePath(path ?? "/admin/users");
}

================
File: src/server/actions/user/update.ts
================
import type { formSchema } from "@/app/admin/users/form";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import type { z } from "zod";

/**
 * Updates a user in the database
 * @param userId User ID to update
 * @param data Updated user data
 * @param path Path to revalidate
 */
export async function updateUser(userId: string, data: z.infer<typeof formSchema>, path?: string) {
	const { user } = await validateRequest();

	if (guard(user, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	await db.update(users).set(data).where(eq(users.id, userId));

	revalidatePath(path ?? "/admin/users");
}

================
File: src/server/actions/users.ts
================
"use server";

import type { formSchema } from "@/app/admin/users/form";
import { validateRequest } from "@/server/auth";
import { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { users } from "@/server/db/schema";
import { IS, IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import type { z } from "zod";

/**
 * @deprecated
 */
export async function updateUser(userId: string, data: z.infer<typeof formSchema>) {
	const { user } = await validateRequest();

	if (guard(user, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	await db.update(users).set(data).where(eq(users.id, userId));

	revalidatePath("/admin/users");
}

/**
 * @deprecated
 */
export async function deleteUser(userId: string) {
	const { user } = await validateRequest();

	if (guard(user, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const dbUser = await db.query.users.findFirst({
		// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
		where: eq(users.id, user!.id),
	});

	if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
		return {
			error: strings.ERROR.PERMISSION,
		};
	}

	const targetUser = await db.query.users.findFirst({
		where: eq(users.id, userId),
	});

	if (!targetUser) {
		return { error: "Benutzer nicht gefunden" };
	}

	if (guard(targetUser, IS, UserRole.ADMIN)) {
		return { error: "Kann keinen Admin löschen" };
	}

	await db.delete(users).where(eq(users.id, userId));

	revalidatePath("/admin/users");
}

================
File: src/server/auth/index.ts
================
import type { UserRole } from "@/server/auth/permissions";
import { db } from "@/server/db";
import { sessions, users } from "@/server/db/schema";
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
import { Lucia, type RegisteredDatabaseUserAttributes, type Session } from "lucia";
import { cookies } from "next/headers";
import { cache } from "react";

//@ts-ignore
const adapter = new DrizzleSQLiteAdapter(db, sessions, users);

export const lucia = new Lucia(adapter, {
	sessionCookie: {
		expires: false,
		attributes: {
			secure: process.env.NODE_ENV === "production",
		},
	},
	getUserAttributes: (attributes) => {
		return {
			id: attributes.id,
			username: attributes.username,
			displayName: attributes.displayName,
			email: attributes.email,
			role: attributes.role,
		};
	},
});

export const validateRequest = cache(
	async (): Promise<{ user: RegisteredDatabaseUserAttributes; session: Session } | { user: null; session: null }> => {
		const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
		if (!sessionId) {
			return {
				user: null,
				session: null,
			};
		}

		const result = await lucia.validateSession(sessionId);
		// next.js throws when you attempt to set cookie when rendering page
		try {
			if (result.session?.fresh) {
				const sessionCookie = lucia.createSessionCookie(result.session.id);
				cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
			}
			if (!result.session) {
				const sessionCookie = lucia.createBlankSessionCookie();
				cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
			}
		} catch {}

		return result as {
			user: RegisteredDatabaseUserAttributes;
			session: Session;
		};
	},
);

declare module "lucia" {
	interface Register {
		Lucia: typeof Lucia;
		DatabaseUserAttributes: {
			id: string;
			github_id: number;
			username: string;
			displayName: string;
			email: string;
			role: UserRole;
		};
	}
}

================
File: src/server/auth/oauth.ts
================
import { GitHub } from "arctic";

export const github = new GitHub(process.env.OAUTH_CLIENT_ID as string, process.env.OAUTH_CLIENT_SECRET as string, {
	enterpriseDomain: "https://git.i.mercedes-benz.com",
});

export interface GitHubUserResult {
	id: number;
	login: string;
	name: string;
	email: string;
}

================
File: src/server/auth/permissions.ts
================
import type { RegisteredDatabaseUserAttributes } from "lucia";

export enum UserRole {
	ADMIN = "admin",
	USER = "user",
	GUEST = "guest",
}

/**
 * @deprecated
 */
export function hasRole(user: RegisteredDatabaseUserAttributes | null | undefined, role: UserRole) {
	return user?.role === role;
}

/**
 * @deprecated
 */
export function translateUserRole(role: UserRole) {
	switch (role) {
		case UserRole.ADMIN:
			return "Administrator";
		case UserRole.USER:
			return "Benutzer";
		case UserRole.GUEST:
			return "Gast";
	}
}

================
File: src/utils/analytics/error-rate.ts
================
import type { printJobs } from "@/server/db/schema";
import type { InferResultType } from "@/utils/drizzle";
import type { InferSelectModel } from "drizzle-orm";

export interface PrinterErrorRate {
	printerId: string;
	name: string;
	errorRate: number; // Error rate as a percentage (0-100)
}

/**
 * Calculates the error rate for print jobs aggregated by printer as a percentage.
 *
 * @param pJobs - Array of print job objects.
 * @returns An array of PrinterErrorRate objects, each containing the printer ID and its error rate.
 */
export function calculatePrinterErrorRate(
	pJobs: InferResultType<"printJobs", { printer: true }>[],
): PrinterErrorRate[] {
	if (pJobs.length === 0) {
		return []; // No jobs, no data.
	}
	const printers = pJobs.map((job) => job.printer);

	// Group jobs by printer ID
	const jobsByPrinter: Record<string, InferSelectModel<typeof printJobs>[]> = pJobs.reduce(
		(acc, job) => {
			if (!acc[job.printerId]) {
				acc[job.printerId] = [];
			}
			acc[job.printerId].push(job);
			return acc;
		},
		{} as Record<string, InferSelectModel<typeof printJobs>[]>,
	);

	// Calculate the error rate for each printer
	const printerErrorRates: PrinterErrorRate[] = Object.entries(jobsByPrinter).map(([printerId, jobs]) => {
		const totalJobs = jobs.length;
		const abortedJobsCount = jobs.filter((job) => job.aborted).length;
		const errorRate = (abortedJobsCount / totalJobs) * 100;

		const printer = printers.find((printer) => printer.id === printerId);
		const printerName = printer ? printer.name : "Unbekannter Drucker";

		return {
			printerId,
			name: printerName,
			errorRate: Number.parseFloat(errorRate.toFixed(2)), // Rounded to two decimal places
		};
	});

	return printerErrorRates;
}

================
File: src/utils/analytics/errors.ts
================
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<string, number>,
		);

	// Convert the result to an array of AbortReasonCount objects
	return Object.entries(abortReasonsCount).map(([abortReason, count]) => ({
		abortReason,
		count,
	}));
}

================
File: src/utils/analytics/forecast.ts
================
import type { InferResultType } from "@/utils/drizzle";

type UsagePerDay = {
	day: number; // 0 (Sunday) to 6 (Saturday)
	usageMinutes: number;
};

function aggregateUsageByDay(jobs: InferResultType<"printJobs">[]): {
	usageData: UsagePerDay[];
	earliestDate: Date;
	latestDate: Date;
} {
	const usagePerDayMap = new Map<number, number>();

	let earliestDate: Date | null = null;
	let latestDate: Date | null = null;

	for (const job of jobs) {
		let remainingDuration = job.durationInMinutes;
		const currentStart = new Date(job.startAt);

		// Update earliest and latest dates
		if (!earliestDate || currentStart < earliestDate) {
			earliestDate = new Date(currentStart);
		}
		const jobEnd = new Date(currentStart);
		jobEnd.setMinutes(jobEnd.getMinutes() + job.durationInMinutes);
		if (!latestDate || jobEnd > latestDate) {
			latestDate = new Date(jobEnd);
		}

		while (remainingDuration > 0) {
			const day = currentStart.getDay();

			// 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,
	}));

	if (earliestDate === null) {
		earliestDate = new Date();
	}

	if (latestDate === null) {
		latestDate = new Date();
	}

	return { usageData, earliestDate: earliestDate, latestDate: latestDate };
}

function countWeekdays(startDate: Date, endDate: Date): number[] {
	const countPerDay = Array(7).fill(0);
	const currentDate = new Date(startDate);
	currentDate.setHours(0, 0, 0, 0); // Ensure starting at midnight
	endDate.setHours(0, 0, 0, 0); // Ensure ending at midnight

	while (currentDate <= endDate) {
		const day = currentDate.getDay();
		countPerDay[day]++;
		currentDate.setDate(currentDate.getDate() + 1);
	}
	return countPerDay;
}

export function forecastPrinterUsage(jobs: InferResultType<"printJobs">[]): number[] {
	const { usageData, earliestDate, latestDate } = aggregateUsageByDay(jobs);

	// Count the number of times each weekday occurs in the data period
	const weekdaysCount = countWeekdays(earliestDate, latestDate);

	const forecasts: number[] = [];
	for (const data of usageData) {
		const dayCount = weekdaysCount[data.day];
		let usagePrediction = data.usageMinutes / dayCount;
		if (Number.isNaN(usagePrediction)) {
			usagePrediction = 0;
		}
		forecasts.push(Math.round(usagePrediction));
	}

	return forecasts;
}

================
File: src/utils/analytics/utilization.ts
================
import type { InferResultType } from "@/utils/drizzle";

export function calculatePrinterUtilization(jobs: InferResultType<"printJobs", { printer: true }>[]) {
	const printers = jobs.reduce<Record<string, string>>((acc, job) => {
		acc[job.printerId] = job.printer.name;
		return acc;
	}, {});

	const usedTimePerPrinter: Record<string, number> = jobs.reduce(
		(acc, job) => {
			acc[job.printer.id] = (acc[job.printer.id] || 0) + job.durationInMinutes;

			return acc;
		},
		{} as Record<string, number>,
	);

	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],
			utilizationPercentage: usedTime / totalTimeInMinutes,
		};
	});

	return printerUtilizationPercentage;
}

================
File: src/utils/analytics/volume.ts
================
import type { printJobs } from "@/server/db/schema";
import { endOfDay, endOfMonth, endOfWeek, startOfDay, startOfMonth, startOfWeek } from "date-fns";
import type { InferSelectModel } from "drizzle-orm";

interface PrintVolumes {
	today: number;
	thisWeek: number;
	thisMonth: number;
}

/**
 * Calculates the number of print jobs for today, this week, and this month.
 *
 * @param printJobs - Array of print job objects.
 * @returns An object with counts of print jobs for today, this week, and this month.
 */
export function calculatePrintVolumes(pJobs: InferSelectModel<typeof printJobs>[]): PrintVolumes {
	const now = new Date();

	// Define time ranges with week starting on Monday
	const timeRanges = {
		today: { start: startOfDay(now), end: endOfDay(now) },
		thisWeek: {
			start: startOfWeek(now, { weekStartsOn: 1 }),
			end: endOfWeek(now, { weekStartsOn: 1 }),
		},
		thisMonth: { start: startOfMonth(now), end: endOfMonth(now) },
	};

	// Initialize counts
	const volumes: PrintVolumes = {
		today: 0,
		thisWeek: 0,
		thisMonth: 0,
	};

	// Iterate over print jobs and count based on time ranges
	for (const job of pJobs) {
		const jobStart = new Date(job.startAt);
		if (jobStart >= timeRanges.today.start && jobStart <= timeRanges.today.end) {
			volumes.today += 1;
		}
		if (jobStart >= timeRanges.thisWeek.start && jobStart <= timeRanges.thisWeek.end) {
			volumes.thisWeek += 1;
		}
		if (jobStart >= timeRanges.thisMonth.start && jobStart <= timeRanges.thisMonth.end) {
			volumes.thisMonth += 1;
		}
	}

	return volumes;
}

================
File: src/utils/drizzle.ts
================
import type * as schema from "@/server/db/schema";
import type { BuildQueryResult, DBQueryConfig, ExtractTablesWithRelations } from "drizzle-orm";

type Schema = typeof schema;
type TSchema = ExtractTablesWithRelations<Schema>;

/**
 * Infer the relation type of a table.
 */
export type IncludeRelation<TableName extends keyof TSchema> = DBQueryConfig<
	"one" | "many",
	boolean,
	TSchema,
	TSchema[TableName]
>["with"];

/**
 * Infer the result type of a query with optional relations.
 */
export type InferResultType<
	TableName extends keyof TSchema,
	With extends IncludeRelation<TableName> | undefined = undefined,
> = BuildQueryResult<
	TSchema,
	TSchema[TableName],
	{
		with: With;
	}
>;

================
File: src/utils/errors.ts
================
import strings from "@/utils/strings";

/**
 * Base error class.
 */
class BaseError extends Error {
	constructor(message?: string) {
		// Pass the message to the Error constructor
		super(message);

		// Set the name of the error
		this.name = this.constructor.name;

		// Capture the stack trace
		if (Error.captureStackTrace) {
			Error.captureStackTrace(this, this.constructor);
		} else {
			this.stack = new Error(message).stack;
		}
	}
}

/**
 * Permission error class.
 */
export class PermissionError extends BaseError {
	constructor() {
		super(strings.ERROR.PERMISSION);
	}
}

/**
 * Authentication error class.
 */
export class AuthenticationError extends BaseError {
	constructor() {
		super(strings.ERROR.NO_SESSION);
	}
}

/**
 * Validation error class.
 */
export class ValidationError extends BaseError {
	constructor() {
		super(strings.ERROR.VALIDATION);
	}
}

/**
 * Not found error class.
 */
export class NotFoundError extends BaseError {
	constructor() {
		super(strings.ERROR.NOT_FOUND);
	}
}

export default BaseError;

================
File: src/utils/fetch.ts
================
export const fetcher = (url: string) => fetch(url).then((response) => response.json());

================
File: src/utils/guard.ts
================
import type { UserRole } from "@/server/auth/permissions";
import type { users } from "@/server/db/schema";
import type { InferSelectModel } from "drizzle-orm";
import type { RegisteredDatabaseUserAttributes } from "lucia";

// Constants for better readability
export const IS = false;
export const IS_NOT = true;

/**
 * Checks if a user has the required role(s).
 * @param user - The user to check.
 * @param negate - Whether to negate the result.
 * @param roleRequirements - The required role(s).
 * @returns Whether the user has the required role(s).
 */
export function guard(
	user: RegisteredDatabaseUserAttributes | InferSelectModel<typeof users> | undefined | null,
	negate: boolean,
	roleRequirements: UserRole | UserRole[],
) {
	// Early return for unauthenticated users
	if (!user) {
		return true;
	}

	// Normalize roleRequirements to an array
	const requiredRoles = Array.isArray(roleRequirements) ? roleRequirements : [roleRequirements];

	// Check if the user's role is in the required roles
	const userHasRequiredRole = requiredRoles.includes(user.role as UserRole);

	// Return the result, negated if necessary
	return negate ? !userHasRequiredRole : userHasRequiredRole;
}

================
File: src/utils/printers.ts
================
import type { InferResultType } from "@/utils/drizzle";

export enum PrinterStatus {
	IDLE = 0,
	OUT_OF_ORDER = 1,
	RESERVED = 2,
}

export function derivePrinterStatus(
	printer: InferResultType<"printers", { printJobs: true }>,
) {
	if (printer.status === PrinterStatus.OUT_OF_ORDER) {
		return PrinterStatus.OUT_OF_ORDER;
	}

	const activePrintJob = printer.printJobs[0];

	if (!activePrintJob || activePrintJob.aborted) {
		return PrinterStatus.IDLE;
	}

	const now = Date.now();
	const startAt = new Date(activePrintJob.startAt).getTime();
	const endAt = startAt + activePrintJob.durationInMinutes * 60 * 1000;
	if (now < endAt) {
		return PrinterStatus.RESERVED;
	}

	return PrinterStatus.IDLE;
}

export function translatePrinterStatus(status: PrinterStatus) {
	switch (status) {
		case PrinterStatus.IDLE:
			return "Verfügbar";
		case PrinterStatus.OUT_OF_ORDER:
			return "Außer Betrieb";
		case PrinterStatus.RESERVED:
			return "Reserviert";
	}
}

================
File: src/utils/strings.ts
================
/**
 * Contains all strings used in the application.
 */
export default {
	ERROR: {
		PERMISSION: "Du besitzt nicht die erforderlichen Berechtigungen um diese Aktion auszuführen.",
		VALIDATION: "Die Eingabe ist ungültig.",
		NOT_FOUND: "Die angeforderten Daten konnten nicht gefunden werden.",
		NO_SESSION: "Du bist nicht angemeldet.",
	},
};

================
File: src/utils/styles.ts
================
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

/**
 * Utility function to merge classes with tailwindcss.
 * @param inputs Classes to merge
 * @returns classes
 */
export function cn(...inputs: ClassValue[]) {
	return twMerge(clsx(inputs));
}

================
File: tailwind.config.ts
================
/** @type {import('tailwindcss').Config} */

const tremor = {
	content: [
		"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}", // Tremor module
	],
	theme: {
		transparent: "transparent",
		current: "currentColor",
		extend: {
			colors: {
				// light mode
				tremor: {
					brand: {
						faint: "#eff6ff", // blue-50
						muted: "#bfdbfe", // blue-200
						subtle: "#60a5fa", // blue-400
						DEFAULT: "#3b82f6", // blue-500
						emphasis: "#1d4ed8", // blue-700
						inverted: "#ffffff", // white
					},
					background: {
						muted: "#f9fafb", // gray-50
						subtle: "#f3f4f6", // gray-100
						DEFAULT: "#ffffff", // white
						emphasis: "#374151", // gray-700
					},
					border: {
						DEFAULT: "#e5e7eb", // gray-200
					},
					ring: {
						DEFAULT: "#e5e7eb", // gray-200
					},
					content: {
						subtle: "#9ca3af", // gray-400
						DEFAULT: "#6b7280", // gray-500
						emphasis: "#374151", // gray-700
						strong: "#111827", // gray-900
						inverted: "#ffffff", // white
					},
				},
				// dark mode
				"dark-tremor": {
					brand: {
						faint: "#0B1229", // custom
						muted: "#172554", // blue-950
						subtle: "#1e40af", // blue-800
						DEFAULT: "#3b82f6", // blue-500
						emphasis: "#60a5fa", // blue-400
						inverted: "#030712", // gray-950
					},
					background: {
						muted: "#131A2B", // custom
						subtle: "#1f2937", // gray-800
						DEFAULT: "#111827", // gray-900
						emphasis: "#d1d5db", // gray-300
					},
					border: {
						DEFAULT: "#1f2937", // gray-800
					},
					ring: {
						DEFAULT: "#1f2937", // gray-800
					},
					content: {
						subtle: "#4b5563", // gray-600
						DEFAULT: "#6b7280", // gray-500
						emphasis: "#e5e7eb", // gray-200
						strong: "#f9fafb", // gray-50
						inverted: "#000000", // black
					},
				},
			},
			boxShadow: {
				// light
				"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
				"tremor-card": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
				"tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
				// dark
				"dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
				"dark-tremor-card": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
				"dark-tremor-dropdown": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
			},
			borderRadius: {
				"tremor-small": "0.375rem",
				"tremor-default": "0.5rem",
				"tremor-full": "9999px",
			},
			fontSize: {
				"tremor-label": ["0.75rem"],
				"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
				"tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
				"tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
			},
		},
	},
	safelist: [
		{
			pattern:
				/^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
			variants: ["hover", "ui-selected"],
		},
		{
			pattern:
				/^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
			variants: ["hover", "ui-selected"],
		},
		{
			pattern:
				/^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
			variants: ["hover", "ui-selected"],
		},
		{
			pattern:
				/^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
		},
		{
			pattern:
				/^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
		},
		{
			pattern:
				/^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
		},
	],
	plugins: [require("@headlessui/tailwindcss")],
};

module.exports = {
	...tremor,
	darkMode: ["class"],
	content: [
		"./pages/**/*.{ts,tsx}",
		"./components/**/*.{ts,tsx}",
		"./app/**/*.{ts,tsx}",
		"./src/**/*.{ts,tsx}",
		...tremor.content,
	],
	theme: {
		...tremor.theme,
		container: {
			center: true,
			padding: "2rem",
			screens: {
				"2xl": "1400px",
			},
		},
		extend: {
			...tremor.theme.extend,
			colors: {
				...tremor.theme.extend.colors,
				border: "hsl(var(--border))",
				input: "hsl(var(--input))",
				ring: "hsl(var(--ring))",
				background: "hsl(var(--background))",
				foreground: "hsl(var(--foreground))",
				primary: {
					DEFAULT: "hsl(var(--primary))",
					foreground: "hsl(var(--primary-foreground))",
				},
				secondary: {
					DEFAULT: "hsl(var(--secondary))",
					foreground: "hsl(var(--secondary-foreground))",
				},
				destructive: {
					DEFAULT: "hsl(var(--destructive))",
					foreground: "hsl(var(--destructive-foreground))",
				},
				muted: {
					DEFAULT: "hsl(var(--muted))",
					foreground: "hsl(var(--muted-foreground))",
				},
				accent: {
					DEFAULT: "hsl(var(--accent))",
					foreground: "hsl(var(--accent-foreground))",
				},
				popover: {
					DEFAULT: "hsl(var(--popover))",
					foreground: "hsl(var(--popover-foreground))",
				},
				card: {
					DEFAULT: "hsl(var(--card))",
					foreground: "hsl(var(--card-foreground))",
				},
			},
			borderRadius: {
				lg: "var(--radius)",
				md: "calc(var(--radius) - 2px)",
				sm: "calc(var(--radius) - 4px)",
				...tremor.theme.extend.borderRadius,
			},
			keyframes: {
				"accordion-down": {
					from: { height: 0 },
					to: { height: "var(--radix-accordion-content-height)" },
				},
				"accordion-up": {
					from: { height: "var(--radix-accordion-content-height)" },
					to: { height: 0 },
				},
			},
			animation: {
				"accordion-down": "accordion-down 0.2s ease-out",
				"accordion-up": "accordion-up 0.2s ease-out",
			},
		},
	},
	plugins: [require("tailwindcss-animate"), ...tremor.plugins],
};

================
File: tsconfig.json
================
{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
