chore: normalize line endings + remove old reservation-platform
This commit is contained in:
parent
5dd1b7b78b
commit
c0adbad773
41
.gitattributes
vendored
41
.gitattributes
vendored
@ -1 +1,40 @@
|
|||||||
packages/reservation-platform/docker/images/*.tar.xz filter=lfs diff=lfs merge=lfs -text
|
frontend/docker/images/*.tar.xz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
# Shell-Skripte: immer LF
|
||||||
|
*.sh text eol=lf
|
||||||
|
|
||||||
|
# Windows-Batch-Dateien: immer CRLF
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Python, JS, HTML etc.: bevorzugt LF (aber nicht erzwungen)
|
||||||
|
*.py text eol=lf
|
||||||
|
*.js text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
|
||||||
|
# Konfigurationsdateien
|
||||||
|
*.env text eol=lf
|
||||||
|
*.conf text eol=lf
|
||||||
|
*.ini text eol=lf
|
||||||
|
|
||||||
|
# Binärdateien
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.webp binary
|
||||||
|
*.pdf binary
|
||||||
|
*.ico binary
|
||||||
|
*.ttf binary
|
||||||
|
*.woff binary
|
||||||
|
*.zip binary
|
||||||
|
*.tar binary
|
||||||
|
*.gz binary
|
||||||
|
*.7z binary
|
||||||
|
*.exe binary
|
||||||
|
*.dll binary
|
||||||
|
@ -1,23 +1,28 @@
|
|||||||
# Dokumentation MYP - Manage your Printer
|
# Dokumentation MYP - Manage your Printer
|
||||||
|
|
||||||
## Projektbeschreibung
|
## Projektbeschreibung
|
||||||
|
|
||||||
MYP (Manage your Printer) ist eine Plattform zur Reservierung von 3D-Druckern, die für die TBA im Werk 040, Berlin-Marienfelde, entwickelt wurde.
|
MYP (Manage your Printer) ist eine Plattform zur Reservierung von 3D-Druckern, die für die TBA im Werk 040, Berlin-Marienfelde, entwickelt wurde.
|
||||||
|
|
||||||
## Projektstruktur
|
## Projektstruktur
|
||||||
|
|
||||||
- `backend/`: Flask-Backend für die API-Anbindung und Datenbankzugriff
|
- `backend/`: Flask-Backend für die API-Anbindung und Datenbankzugriff
|
||||||
- `packages/reservation-platform/`: Next.js Frontend für die Benutzeroberfläche
|
- `frontend/`: Next.js Frontend für die Benutzeroberfläche
|
||||||
- `docs/`: Ausführliche Dokumentationen, Datenbankschema und Diagramme
|
- `docs/`: Ausführliche Dokumentationen, Datenbankschema und Diagramme
|
||||||
- `scripts/`: Deployment- und Setup-Skripte
|
- `scripts/`: Deployment- und Setup-Skripte
|
||||||
- `logs/`: Fehlerprotokolle und Logs
|
- `logs/`: Fehlerprotokolle und Logs
|
||||||
|
|
||||||
## Umfassende Dokumentation
|
## Umfassende Dokumentation
|
||||||
|
|
||||||
Detaillierte Dokumentationen finden Sie in den folgenden Dateien:
|
Detaillierte Dokumentationen finden Sie in den folgenden Dateien:
|
||||||
|
|
||||||
- [Technische Dokumentation](docs/README.md)
|
- [Technische Dokumentation](docs/README.md)
|
||||||
- [Datenbankstruktur](docs/MYP.dbml)
|
- [Datenbankstruktur](docs/MYP.dbml)
|
||||||
- [Aktueller Projektstand](docs/Aktueller%20Stand.md)
|
- [Aktueller Projektstand](docs/Aktueller%20Stand.md)
|
||||||
- [IHK-Dokumentation](docs/Dokumentation_IHK.md)
|
- [IHK-Dokumentation](docs/Dokumentation_IHK.md)
|
||||||
|
|
||||||
## Herausforderungen und Komplikationen
|
## Herausforderungen und Komplikationen
|
||||||
|
|
||||||
- Netzwerkanbindung
|
- Netzwerkanbindung
|
||||||
- Ermitteln der Schnittstellen der Drucker
|
- Ermitteln der Schnittstellen der Drucker
|
||||||
- Auswahl der Anbindung, Entwickeln eines Netzwerkkonzeptes
|
- Auswahl der Anbindung, Entwickeln eines Netzwerkkonzeptes
|
||||||
@ -27,6 +32,7 @@ Detaillierte Dokumentationen finden Sie in den folgenden Dateien:
|
|||||||
- Netzwerk einrichten, Frontend anbinden
|
- Netzwerk einrichten, Frontend anbinden
|
||||||
|
|
||||||
## Verwendete Technologien
|
## Verwendete Technologien
|
||||||
|
|
||||||
- Backend: Python, Flask
|
- Backend: Python, Flask
|
||||||
- Frontend: Next.js, React, TypeScript
|
- Frontend: Next.js, React, TypeScript
|
||||||
- Datenbank: SQL
|
- Datenbank: SQL
|
||||||
@ -34,7 +40,8 @@ Detaillierte Dokumentationen finden Sie in den folgenden Dateien:
|
|||||||
- Raspberry Pi für Druckersteuerung
|
- Raspberry Pi für Druckersteuerung
|
||||||
|
|
||||||
## Installation und Einsatz
|
## Installation und Einsatz
|
||||||
|
|
||||||
Installation und Einrichtung werden durch die Skripte im Verzeichnis `scripts/` unterstützt.
|
Installation und Einrichtung werden durch die Skripte im Verzeichnis `scripts/` unterstützt.
|
||||||
|
|
||||||
- `scripts/setup/`: Einrichtungsskripte für Backend, Docker und OAuth
|
- `scripts/setup/`: Einrichtungsskripte für Backend, Docker und OAuth
|
||||||
- `scripts/deployment/`: Bereitstellungsskripte für Raspberry Pi
|
- `scripts/deployment/`: Bereitstellungsskripte für Raspberry Pi
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
MYP *(Manage your Printer)* ist eine Plattform zur Reservierung von 3D-Druckern, die für die TBA im Werk 040, Berlin-Marienfelde, entwickelt wurde.
|
MYP *(Manage your Printer)* ist eine Plattform zur Reservierung von 3D-Druckern, die für die TBA im Werk 040, Berlin-Marienfelde, entwickelt wurde.
|
||||||
|
|
||||||
> Frontend: https://git.i.mercedes-benz.com/TBA-Berlin-FI/MYP/tree/main/packages/reservation-platform
|
> Frontend: https://git.i.mercedes-benz.com/TBA-Berlin-FI/MYP/tree/main/frontend
|
||||||
|
|
||||||
> ⚠️ MYP ist zzt. in Entwicklung
|
> ⚠️ MYP ist zzt. in Entwicklung
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ Dies ist das Backend für das MYP (Manage Your Printer) Projekt, ein IHK-Abschlu
|
|||||||
```
|
```
|
||||||
python app.py
|
python app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
||||||
|
|
||||||
### Mit Docker
|
### Mit Docker
|
||||||
@ -116,6 +116,7 @@ Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
|||||||
## Datenmodell
|
## Datenmodell
|
||||||
|
|
||||||
### Benutzer (User)
|
### Benutzer (User)
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
- id (String UUID, Primary Key)
|
||||||
- username (String, Unique)
|
- username (String, Unique)
|
||||||
- password_hash (String)
|
- password_hash (String)
|
||||||
@ -124,11 +125,13 @@ Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
|||||||
- role (String, 'admin', 'user' oder 'guest')
|
- role (String, 'admin', 'user' oder 'guest')
|
||||||
|
|
||||||
### Session
|
### Session
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
- id (String UUID, Primary Key)
|
||||||
- user_id (String UUID, Foreign Key zu User)
|
- user_id (String UUID, Foreign Key zu User)
|
||||||
- expires_at (DateTime)
|
- expires_at (DateTime)
|
||||||
|
|
||||||
### Drucker (Printer)
|
### Drucker (Printer)
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
- id (String UUID, Primary Key)
|
||||||
- name (String)
|
- name (String)
|
||||||
- description (Text)
|
- description (Text)
|
||||||
@ -136,6 +139,7 @@ Die Anwendung ist dann unter http://localhost:5000 erreichbar.
|
|||||||
- ip_address (String, IP-Adresse der Tapo-Steckdose)
|
- ip_address (String, IP-Adresse der Tapo-Steckdose)
|
||||||
|
|
||||||
### Druckauftrag (PrintJob)
|
### Druckauftrag (PrintJob)
|
||||||
|
|
||||||
- id (String UUID, Primary Key)
|
- id (String UUID, Primary Key)
|
||||||
- printer_id (String UUID, Foreign Key zu Printer)
|
- printer_id (String UUID, Foreign Key zu Printer)
|
||||||
- user_id (String UUID, Foreign Key zu User)
|
- user_id (String UUID, Foreign Key zu User)
|
||||||
@ -182,4 +186,4 @@ Die Anwendung beinhaltet einen CLI-Befehl `flask check-jobs`, der regelmäßig a
|
|||||||
|
|
||||||
## Kompatibilität mit dem Frontend
|
## Kompatibilität mit dem Frontend
|
||||||
|
|
||||||
Das Backend wurde speziell für die Kompatibilität mit dem bestehenden Frontend entwickelt, welches in `/packages/reservation-platform` zu finden ist. Die API-Endpunkte und Datenstrukturen sind so gestaltet, dass sie nahtlos mit dem Frontend zusammenarbeiten.
|
Das Backend wurde speziell für die Kompatibilität mit dem bestehenden Frontend entwickelt, welches in `/frontend` zu finden ist. Die API-Endpunkte und Datenstrukturen sind so gestaltet, dass sie nahtlos mit dem Frontend zusammenarbeiten.
|
||||||
|
@ -1 +1,51 @@
|
|||||||
|
# Aufräumarbeiten MYP-Projekt (19.05.2025)
|
||||||
|
|
||||||
|
## Durchgeführte Änderungen
|
||||||
|
|
||||||
|
### Verzeichnisstruktur
|
||||||
|
|
||||||
|
- Skriptdateien in logische Kategorien reorganisiert:
|
||||||
|
- `scripts/setup/`: Einrichtungsskripte
|
||||||
|
- `scripts/deployment/`: Bereitstellungsskripte für Raspberry Pi
|
||||||
|
- Neue Verzeichnisse erstellt:
|
||||||
|
- `logs/`: Für Fehlerprotokolle und Logdateien
|
||||||
|
- `config/secure/`: Für sensible Konfigurationsdaten
|
||||||
|
|
||||||
|
### Dokumentation
|
||||||
|
|
||||||
|
- Zentrale `Dokumentation.md` aktualisiert und erweitert
|
||||||
|
- Entwicklungsrichtlinien von `CLAUDE.md` nach `docs/Entwicklungsrichtlinien.md` verschoben
|
||||||
|
- README.md korrigiert und Git-Konfliktmarkierungen entfernt
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
|
||||||
|
- Sensible Daten (CREDENTIALS) in `config/secure/` verschoben
|
||||||
|
- `.gitignore` aktualisiert, um sensible Dateien und temporäre Dateien auszuschließen
|
||||||
|
|
||||||
|
### Dateiorganisation
|
||||||
|
|
||||||
|
- Fehlerlogs in `logs/` verschoben
|
||||||
|
- Temporäre Dateien bereinigt
|
||||||
|
- Skript-Dateien in sinnvolle Kategorien einsortiert
|
||||||
|
|
||||||
|
## Projektstruktur nach Aufräumarbeiten
|
||||||
|
|
||||||
|
```
|
||||||
|
Projektarbeit-MYP/
|
||||||
|
├── backend/ # Flask-Backend
|
||||||
|
├── config/
|
||||||
|
│ └── secure/ # Sensible Konfigurationen
|
||||||
|
├── docs/ # Projektdokumentation
|
||||||
|
├── frontend/
|
||||||
|
├── logs/ # Fehlerprotokolle
|
||||||
|
└── scripts/
|
||||||
|
├── deployment/ # Raspberry Pi Deployment
|
||||||
|
└── setup/ # Einrichtungsskripte
|
||||||
|
```
|
||||||
|
|
||||||
|
## Empfehlungen für zukünftige Arbeiten
|
||||||
|
|
||||||
|
- Gemeinsames Datenbankmodell zwischen Backend und Frontend überarbeiten
|
||||||
|
- Weitere Dokumentation der API-Schnittstellen erstellen
|
||||||
|
- Testabdeckung erhöhen
|
||||||
|
- Deployment-Prozess automatisieren
|
||||||
|
@ -1,44 +1,49 @@
|
|||||||
# MYP Project Development Guidelines
|
# MYP Project Development Guidelines
|
||||||
|
|
||||||
## System Architecture
|
## System Architecture
|
||||||
- **Frontend**:
|
|
||||||
- Located in `packages/reservation-platform`
|
- **Frontend**:
|
||||||
|
|
||||||
|
- Located in `frontend`
|
||||||
- Runs on a Raspberry Pi connected to company network
|
- Runs on a Raspberry Pi connected to company network
|
||||||
- Has internet access on one interface
|
- Has internet access on one interface
|
||||||
- Connected via LAN to an offline network
|
- Connected via LAN to an offline network
|
||||||
- Serves as the user interface
|
- Serves as the user interface
|
||||||
- Developed by another apprentice as part of IHK project work
|
- Developed by another apprentice as part of IHK project work
|
||||||
|
|
||||||
- **Backend**:
|
- **Backend**:
|
||||||
|
|
||||||
- Located in `backend` directory
|
- Located in `backend` directory
|
||||||
- Flask application running on a separate Raspberry Pi
|
- Flask application running on a separate Raspberry Pi
|
||||||
- Connected only to the offline network
|
- Connected only to the offline network
|
||||||
- Communicates with WiFi smart plugs
|
- Communicates with WiFi smart plugs
|
||||||
- Part of my IHK project work for digital networking qualification
|
- Part of my IHK project work for digital networking qualification
|
||||||
|
|
||||||
- **Printers/Smart Plugs**:
|
- **Printers/Smart Plugs**:
|
||||||
|
|
||||||
- Printers can only be controlled (on/off) via WiFi smart plugs
|
- Printers can only be controlled (on/off) via WiFi smart plugs
|
||||||
- No other control mechanisms available
|
- No other control mechanisms available
|
||||||
- Smart plugs and printers are equivalent in the system context
|
- Smart plugs and printers are equivalent in the system context
|
||||||
|
|
||||||
## Build/Run Commands
|
## Build/Run Commands
|
||||||
|
|
||||||
- Backend: `cd backend && source venv/bin/activate && python app.py`
|
- Backend: `cd backend && source venv/bin/activate && python app.py`
|
||||||
- Frontend: `cd packages/reservation-platform && pnpm dev`
|
- Frontend: `cd frontend && pnpm dev`
|
||||||
- Run tests: `cd backend && python -m unittest development/tests/tests.py`
|
- Run tests: `cd backend && python -m unittest development/tests/tests.py`
|
||||||
- Run single test: `cd backend && python -m unittest development.tests.tests.MYPBackendTestCase.test_name`
|
- Run single test: `cd backend && python -m unittest development.tests.tests.MYPBackendTestCase.test_name`
|
||||||
- Check jobs manually: `cd backend && source venv/bin/activate && flask check-jobs`
|
- Check jobs manually: `cd backend && source venv/bin/activate && flask check-jobs`
|
||||||
- Lint frontend: `cd packages/reservation-platform && pnpm lint`
|
- Lint frontend: `cd frontend && pnpm lint`
|
||||||
- Format frontend: `cd packages/reservation-platform && npx @biomejs/biome format --write ./src`
|
- Format frontend: `cd frontend && npx @biomejs/biome format --write ./src`
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
- **Python Backend**:
|
- **Python Backend**:
|
||||||
|
|
||||||
- Use PEP 8 conventions, 4-space indentation
|
- Use PEP 8 conventions, 4-space indentation
|
||||||
- Line width: 100 characters max
|
- Line width: 100 characters max
|
||||||
- Add docstrings to functions and classes
|
- Add docstrings to functions and classes
|
||||||
- Error handling: Use try/except with specific exceptions
|
- Error handling: Use try/except with specific exceptions
|
||||||
- Naming: snake_case for functions/variables, PascalCase for classes
|
- Naming: snake_case for functions/variables, PascalCase for classes
|
||||||
|
|
||||||
- **Frontend (Next.js/TypeScript)**:
|
- **Frontend (Next.js/TypeScript)**:
|
||||||
|
|
||||||
- Use Biome for formatting and linting (line width: 120 chars)
|
- Use Biome for formatting and linting (line width: 120 chars)
|
||||||
- Organize imports automatically with Biome
|
- Organize imports automatically with Biome
|
||||||
- Use TypeScript types for all code
|
- Use TypeScript types for all code
|
||||||
@ -46,8 +51,9 @@
|
|||||||
- Naming: camelCase for functions/variables, PascalCase for components
|
- Naming: camelCase for functions/variables, PascalCase for components
|
||||||
|
|
||||||
## Work Guidelines
|
## Work Guidelines
|
||||||
|
|
||||||
- All changes must be committed to git
|
- All changes must be committed to git
|
||||||
- Work efficiently and cost-effectively
|
- Work efficiently and cost-effectively
|
||||||
- Don't repeatedly try the same solution if it doesn't work
|
- Don't repeatedly try the same solution if it doesn't work
|
||||||
- Create and check notes when encountering issues
|
- Create and check notes when encountering issues
|
||||||
- Clearly communicate if something is not possible so I can handle it manually
|
- Clearly communicate if something is not possible so I can handle it manually
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
# 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/
|
|
43
packages/reservation-platform/.gitignore
vendored
43
packages/reservation-platform/.gitignore
vendored
@ -1,43 +0,0 @@
|
|||||||
# 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
|
|
@ -1,34 +0,0 @@
|
|||||||
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"]
|
|
@ -1,32 +0,0 @@
|
|||||||
# 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)
|
|
||||||
```
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "myp-frontend-debug-server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Debug-Server für das MYP Frontend",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node src/index.js",
|
|
||||||
"dev": "nodemon src/index.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"systeminformation": "^5.18.15",
|
|
||||||
"ejs": "^3.1.9",
|
|
||||||
"os-utils": "^0.0.14",
|
|
||||||
"node-fetch": "^2.6.7",
|
|
||||||
"socket.io": "^4.7.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^3.0.2"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,186 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>MYP Frontend Debug-Server</title>
|
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠️</text></svg>">
|
|
||||||
<script src="/socket.io/socket.io.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<div class="header-content">
|
|
||||||
<h1>MYP Frontend Debug-Server</h1>
|
|
||||||
<div class="server-info">
|
|
||||||
<span id="hostname"><strong>Hostname:</strong> <%= hostname %></span>
|
|
||||||
<% for(const [interface, ip] of Object.entries(ipAddresses)) { %>
|
|
||||||
<span><strong><%= interface %>:</strong> <%= ip %></span>
|
|
||||||
<% } %>
|
|
||||||
<span id="current-time"><strong>Zeitstempel:</strong> <%= timestamp %></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<nav>
|
|
||||||
<button class="nav-button active" data-panel="system">System</button>
|
|
||||||
<button class="nav-button" data-panel="network">Netzwerk</button>
|
|
||||||
<button class="nav-button" data-panel="services">Dienste</button>
|
|
||||||
<button class="nav-button" data-panel="tools">Diagnose-Tools</button>
|
|
||||||
<button class="nav-button" data-panel="realtime">Echtzeit-Monitor</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<!-- System Panel -->
|
|
||||||
<section id="system" class="panel active">
|
|
||||||
<h2>Systeminformationen</h2>
|
|
||||||
<div class="loader" id="system-loader">Lade Daten...</div>
|
|
||||||
<div class="card-container" id="system-container" style="display: none;">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Betriebssystem</h3>
|
|
||||||
<div id="os-info"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Prozessor</h3>
|
|
||||||
<div id="cpu-info"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Arbeitsspeicher</h3>
|
|
||||||
<div id="memory-info"></div>
|
|
||||||
<div class="progress-container">
|
|
||||||
<div id="memory-bar" class="progress-bar"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Festplatten</h3>
|
|
||||||
<div id="disk-info"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Network Panel -->
|
|
||||||
<section id="network" class="panel">
|
|
||||||
<h2>Netzwerkinformationen</h2>
|
|
||||||
<div class="loader" id="network-loader">Lade Daten...</div>
|
|
||||||
<div class="card-container" id="network-container" style="display: none;">
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Netzwerkschnittstellen</h3>
|
|
||||||
<div id="network-interfaces"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>DNS-Server</h3>
|
|
||||||
<div id="dns-servers"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Standard-Gateway</h3>
|
|
||||||
<div id="default-gateway"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Netzwerkstatistiken</h3>
|
|
||||||
<div id="network-stats"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Services Panel -->
|
|
||||||
<section id="services" class="panel">
|
|
||||||
<h2>Dienststatus</h2>
|
|
||||||
<div class="loader" id="services-loader">Lade Daten...</div>
|
|
||||||
<div class="card-container" id="services-container" style="display: none;">
|
|
||||||
<div class="card">
|
|
||||||
<h3>Frontend (Next.js)</h3>
|
|
||||||
<div id="frontend-status"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h3>Backend (Flask)</h3>
|
|
||||||
<div id="backend-status"></div>
|
|
||||||
</div>
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Docker Container</h3>
|
|
||||||
<div id="docker-container-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Tools Panel -->
|
|
||||||
<section id="tools" class="panel">
|
|
||||||
<h2>Netzwerk-Diagnosetools</h2>
|
|
||||||
<div class="card-container">
|
|
||||||
<div class="card tool-card">
|
|
||||||
<h3>Ping</h3>
|
|
||||||
<div class="tool-input">
|
|
||||||
<input type="text" id="ping-host" placeholder="Hostname oder IP-Adresse">
|
|
||||||
<button id="ping-button">Ping</button>
|
|
||||||
</div>
|
|
||||||
<pre id="ping-result" class="result-box">Geben Sie einen Hostnamen oder eine IP-Adresse ein...</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card tool-card">
|
|
||||||
<h3>Traceroute</h3>
|
|
||||||
<div class="tool-input">
|
|
||||||
<input type="text" id="traceroute-host" placeholder="Hostname oder IP-Adresse">
|
|
||||||
<button id="traceroute-button">Traceroute</button>
|
|
||||||
</div>
|
|
||||||
<pre id="traceroute-result" class="result-box">Geben Sie einen Hostnamen oder eine IP-Adresse ein...</pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card tool-card">
|
|
||||||
<h3>DNS-Lookup</h3>
|
|
||||||
<div class="tool-input">
|
|
||||||
<input type="text" id="nslookup-host" placeholder="Hostname oder IP-Adresse">
|
|
||||||
<button id="nslookup-button">NSLookup</button>
|
|
||||||
</div>
|
|
||||||
<pre id="nslookup-result" class="result-box">Geben Sie einen Hostnamen oder eine IP-Adresse ein...</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Realtime Panel -->
|
|
||||||
<section id="realtime" class="panel">
|
|
||||||
<h2>Echtzeit-Monitoring</h2>
|
|
||||||
<div class="card-container">
|
|
||||||
<div class="card">
|
|
||||||
<h3>CPU-Auslastung</h3>
|
|
||||||
<div class="gauge-container">
|
|
||||||
<div class="gauge">
|
|
||||||
<div class="gauge-body">
|
|
||||||
<div id="cpu-gauge" class="gauge-fill"></div>
|
|
||||||
<div class="gauge-cover"></div>
|
|
||||||
</div>
|
|
||||||
<div id="cpu-percentage" class="gauge-value">0%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="cpu-cores-container"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3>Arbeitsspeicher</h3>
|
|
||||||
<div class="gauge-container">
|
|
||||||
<div class="gauge">
|
|
||||||
<div class="gauge-body">
|
|
||||||
<div id="memory-gauge" class="gauge-fill"></div>
|
|
||||||
<div class="gauge-cover"></div>
|
|
||||||
</div>
|
|
||||||
<div id="memory-percentage" class="gauge-value">0%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="memory-details"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card full-width">
|
|
||||||
<h3>Netzwerkdurchsatz</h3>
|
|
||||||
<div id="network-throughput"></div>
|
|
||||||
<canvas id="network-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© 2025 MYP (Manage your Printer) | Debug-Server v1.0.0</p>
|
|
||||||
<p>Netzwerk- und Systemdiagnose-Tool für das MYP Frontend</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="/js/script.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,471 +0,0 @@
|
|||||||
// Frontend Debug-Server für MYP
|
|
||||||
const express = require('express');
|
|
||||||
const path = require('path');
|
|
||||||
const http = require('http');
|
|
||||||
const si = require('systeminformation');
|
|
||||||
const os = require('os');
|
|
||||||
const osUtils = require('os-utils');
|
|
||||||
const { exec } = require('child_process');
|
|
||||||
const fetch = require('node-fetch');
|
|
||||||
const socketIo = require('socket.io');
|
|
||||||
|
|
||||||
// Konfiguration
|
|
||||||
const app = express();
|
|
||||||
const server = http.createServer(app);
|
|
||||||
const io = socketIo(server);
|
|
||||||
const PORT = 6666;
|
|
||||||
const FRONTEND_PORT = 3000;
|
|
||||||
const FRONTEND_HOST = 'localhost';
|
|
||||||
const BACKEND_HOST = 'localhost';
|
|
||||||
const BACKEND_PORT = 5000;
|
|
||||||
|
|
||||||
// View Engine einrichten
|
|
||||||
app.set('view engine', 'ejs');
|
|
||||||
app.set('views', path.join(__dirname, '../public/views'));
|
|
||||||
app.use(express.static(path.join(__dirname, '../public')));
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Hauptseite rendern
|
|
||||||
app.get('/', async (req, res) => {
|
|
||||||
const hostname = os.hostname();
|
|
||||||
const networkInterfaces = os.networkInterfaces();
|
|
||||||
const ipAddresses = {};
|
|
||||||
|
|
||||||
// IP-Adressen sammeln
|
|
||||||
Object.keys(networkInterfaces).forEach(interfaceName => {
|
|
||||||
const interfaceInfo = networkInterfaces[interfaceName];
|
|
||||||
const ipv4Addresses = interfaceInfo.filter(info => info.family === 'IPv4');
|
|
||||||
if (ipv4Addresses.length > 0) {
|
|
||||||
ipAddresses[interfaceName] = ipv4Addresses[0].address;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rendere die Hauptseite mit Basisdaten
|
|
||||||
res.render('index', {
|
|
||||||
hostname: hostname,
|
|
||||||
ipAddresses: ipAddresses,
|
|
||||||
timestamp: new Date().toLocaleString('de-DE'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// API-Endpunkte
|
|
||||||
|
|
||||||
// Systeminformationen
|
|
||||||
app.get('/api/system', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const [cpu, mem, osInfo, diskLayout, fsSize] = await Promise.all([
|
|
||||||
si.cpu(),
|
|
||||||
si.mem(),
|
|
||||||
si.osInfo(),
|
|
||||||
si.diskLayout(),
|
|
||||||
si.fsSize()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
cpu: {
|
|
||||||
manufacturer: cpu.manufacturer,
|
|
||||||
brand: cpu.brand,
|
|
||||||
speed: cpu.speed,
|
|
||||||
cores: cpu.cores,
|
|
||||||
physicalCores: cpu.physicalCores
|
|
||||||
},
|
|
||||||
memory: {
|
|
||||||
total: formatBytes(mem.total),
|
|
||||||
free: formatBytes(mem.free),
|
|
||||||
used: formatBytes(mem.used),
|
|
||||||
usedPercent: Math.round(mem.used / mem.total * 100)
|
|
||||||
},
|
|
||||||
os: {
|
|
||||||
platform: osInfo.platform,
|
|
||||||
distro: osInfo.distro,
|
|
||||||
release: osInfo.release,
|
|
||||||
arch: osInfo.arch,
|
|
||||||
uptime: formatUptime(os.uptime())
|
|
||||||
},
|
|
||||||
filesystem: fsSize.map(fs => ({
|
|
||||||
fs: fs.fs,
|
|
||||||
type: fs.type,
|
|
||||||
size: formatBytes(fs.size),
|
|
||||||
used: formatBytes(fs.used),
|
|
||||||
available: formatBytes(fs.available),
|
|
||||||
mount: fs.mount,
|
|
||||||
usePercent: Math.round(fs.use)
|
|
||||||
})),
|
|
||||||
disks: diskLayout.map(disk => ({
|
|
||||||
device: disk.device,
|
|
||||||
type: disk.type,
|
|
||||||
name: disk.name,
|
|
||||||
size: formatBytes(disk.size)
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Abrufen der Systemdaten:', error);
|
|
||||||
res.status(500).json({ error: 'Fehler beim Abrufen der Systemdaten' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Netzwerkinformationen
|
|
||||||
app.get('/api/network', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const [netInterfaces, netStats] = await Promise.all([
|
|
||||||
si.networkInterfaces(),
|
|
||||||
si.networkStats()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const dns = await getDnsServers();
|
|
||||||
const gateway = await getDefaultGateway();
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
interfaces: netInterfaces.map(iface => ({
|
|
||||||
iface: iface.iface,
|
|
||||||
ip4: iface.ip4,
|
|
||||||
ip6: iface.ip6,
|
|
||||||
mac: iface.mac,
|
|
||||||
internal: iface.internal,
|
|
||||||
operstate: iface.operstate,
|
|
||||||
type: iface.type,
|
|
||||||
speed: iface.speed,
|
|
||||||
dhcp: iface.dhcp
|
|
||||||
})),
|
|
||||||
stats: netStats.map(stat => ({
|
|
||||||
iface: stat.iface,
|
|
||||||
rx_bytes: formatBytes(stat.rx_bytes),
|
|
||||||
tx_bytes: formatBytes(stat.tx_bytes),
|
|
||||||
rx_sec: formatBytes(stat.rx_sec),
|
|
||||||
tx_sec: formatBytes(stat.tx_sec)
|
|
||||||
})),
|
|
||||||
dns: dns,
|
|
||||||
gateway: gateway
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Abrufen der Netzwerkdaten:', error);
|
|
||||||
res.status(500).json({ error: 'Fehler beim Abrufen der Netzwerkdaten' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dienststatus
|
|
||||||
app.get('/api/services', async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Prüfen ob Frontend (Next.js) läuft
|
|
||||||
const frontendStatus = await checkServiceStatus(FRONTEND_HOST, FRONTEND_PORT);
|
|
||||||
|
|
||||||
// Prüfen ob Backend (Flask) läuft
|
|
||||||
const backendStatus = await checkServiceStatus(BACKEND_HOST, BACKEND_PORT);
|
|
||||||
|
|
||||||
// Docker-Container Status abrufen
|
|
||||||
const containers = await getDockerContainers();
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
frontend: {
|
|
||||||
name: 'Next.js Frontend',
|
|
||||||
status: frontendStatus ? 'online' : 'offline',
|
|
||||||
port: FRONTEND_PORT,
|
|
||||||
host: FRONTEND_HOST
|
|
||||||
},
|
|
||||||
backend: {
|
|
||||||
name: 'Flask Backend',
|
|
||||||
status: backendStatus ? 'online' : 'offline',
|
|
||||||
port: BACKEND_PORT,
|
|
||||||
host: BACKEND_HOST
|
|
||||||
},
|
|
||||||
docker: {
|
|
||||||
containers: containers
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Abrufen der Dienststatus:', error);
|
|
||||||
res.status(500).json({ error: 'Fehler beim Abrufen der Dienststatus' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ping-Endpunkt für Netzwerkdiagnose
|
|
||||||
app.get('/api/ping/:host', (req, res) => {
|
|
||||||
const host = req.params.host;
|
|
||||||
|
|
||||||
// Sicherheitscheck für den Hostnamen
|
|
||||||
if (!isValidHostname(host)) {
|
|
||||||
return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping-Befehl ausführen
|
|
||||||
exec(`ping -n 4 ${host}`, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
output: stderr || stdout,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
output: stdout
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Traceroute-Endpunkt für Netzwerkdiagnose
|
|
||||||
app.get('/api/traceroute/:host', (req, res) => {
|
|
||||||
const host = req.params.host;
|
|
||||||
|
|
||||||
// Sicherheitscheck für den Hostnamen
|
|
||||||
if (!isValidHostname(host)) {
|
|
||||||
return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traceroute-Befehl ausführen (Windows: tracert, Unix: traceroute)
|
|
||||||
const command = process.platform === 'win32' ? 'tracert' : 'traceroute';
|
|
||||||
exec(`${command} ${host}`, (error, stdout, stderr) => {
|
|
||||||
// Traceroute kann einen Nicht-Null-Exit-Code zurückgeben, selbst wenn es teilweise erfolgreich ist
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
output: stdout,
|
|
||||||
error: stderr
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// DNS-Lookup-Endpunkt für Netzwerkdiagnose
|
|
||||||
app.get('/api/nslookup/:host', (req, res) => {
|
|
||||||
const host = req.params.host;
|
|
||||||
|
|
||||||
// Sicherheitscheck für den Hostnamen
|
|
||||||
if (!isValidHostname(host)) {
|
|
||||||
return res.status(400).json({ error: 'Ungültiger Hostname oder IP-Adresse' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// NSLookup-Befehl ausführen
|
|
||||||
exec(`nslookup ${host}`, (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
output: stderr || stdout,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
output: stdout
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Echtzeit-Updates über WebSockets
|
|
||||||
io.on('connection', (socket) => {
|
|
||||||
console.log('Neue WebSocket-Verbindung');
|
|
||||||
|
|
||||||
// CPU- und Arbeitsspeichernutzung im Intervall senden
|
|
||||||
const systemMonitorInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const [cpu, mem] = await Promise.all([
|
|
||||||
si.currentLoad(),
|
|
||||||
si.mem()
|
|
||||||
]);
|
|
||||||
|
|
||||||
socket.emit('system-stats', {
|
|
||||||
cpu: {
|
|
||||||
load: Math.round(cpu.currentLoad),
|
|
||||||
cores: cpu.cpus.map(core => Math.round(core.load))
|
|
||||||
},
|
|
||||||
memory: {
|
|
||||||
total: mem.total,
|
|
||||||
used: mem.used,
|
|
||||||
free: mem.free,
|
|
||||||
usedPercent: Math.round(mem.used / mem.total * 100)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Senden der Systemstatistiken:', error);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
// Netzwerkstatistiken im Intervall senden
|
|
||||||
const networkMonitorInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const netStats = await si.networkStats();
|
|
||||||
|
|
||||||
socket.emit('network-stats', {
|
|
||||||
stats: netStats.map(stat => ({
|
|
||||||
iface: stat.iface,
|
|
||||||
rx_bytes: stat.rx_bytes,
|
|
||||||
tx_bytes: stat.tx_bytes,
|
|
||||||
rx_sec: stat.rx_sec,
|
|
||||||
tx_sec: stat.tx_sec
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Senden der Netzwerkstatistiken:', error);
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
// Aufräumen, wenn die Verbindung getrennt wird
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
console.log('WebSocket-Verbindung getrennt');
|
|
||||||
clearInterval(systemMonitorInterval);
|
|
||||||
clearInterval(networkMonitorInterval);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hilfsfunktionen
|
|
||||||
|
|
||||||
// Bytes in lesbare Größen formatieren
|
|
||||||
function formatBytes(bytes, decimals = 2) {
|
|
||||||
if (bytes === 0) return '0 B';
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uptime in lesbare Zeit formatieren
|
|
||||||
function formatUptime(seconds) {
|
|
||||||
const days = Math.floor(seconds / 86400);
|
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
|
|
||||||
return `${days} Tage, ${hours} Stunden, ${minutes} Minuten, ${secs} Sekunden`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service-Status überprüfen
|
|
||||||
async function checkServiceStatus(host, port) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const socket = new (require('net').Socket)();
|
|
||||||
|
|
||||||
socket.setTimeout(1000);
|
|
||||||
|
|
||||||
socket.on('connect', () => {
|
|
||||||
socket.destroy();
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('timeout', () => {
|
|
||||||
socket.destroy();
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('error', () => {
|
|
||||||
socket.destroy();
|
|
||||||
resolve(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.connect(port, host);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Docker-Container abfragen
|
|
||||||
async function getDockerContainers() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
exec('docker ps --format "{{.ID}},{{.Image}},{{.Status}},{{.Ports}},{{.Names}}"', (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
resolve([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const containers = [];
|
|
||||||
const lines = stdout.trim().split('\n');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line) {
|
|
||||||
const [id, image, status, ports, name] = line.split(',');
|
|
||||||
containers.push({ id, image, status, ports, name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(containers);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// DNS-Server abfragen
|
|
||||||
async function getDnsServers() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
// Windows: DNS-Server über PowerShell abfragen
|
|
||||||
exec('powershell.exe -Command "Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object -ExpandProperty ServerAddresses"', (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
resolve(['DNS-Server konnten nicht ermittelt werden']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const servers = stdout.trim().split('\r\n').filter(Boolean);
|
|
||||||
resolve(servers);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Unix: DNS-Server aus /etc/resolv.conf lesen
|
|
||||||
exec('cat /etc/resolv.conf | grep nameserver | cut -d " " -f 2', (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
resolve(['DNS-Server konnten nicht ermittelt werden']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const servers = stdout.trim().split('\n').filter(Boolean);
|
|
||||||
resolve(servers);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard-Gateway abfragen
|
|
||||||
async function getDefaultGateway() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
// Windows: Gateway über PowerShell abfragen
|
|
||||||
exec('powershell.exe -Command "Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select-Object -ExpandProperty NextHop"', (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
resolve('Gateway konnte nicht ermittelt werden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(stdout.trim());
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Unix: Gateway aus den Routentabellen lesen
|
|
||||||
exec("ip route | grep default | awk '{print $3}'", (error, stdout) => {
|
|
||||||
if (error) {
|
|
||||||
resolve('Gateway konnte nicht ermittelt werden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(stdout.trim());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validierung des Hostnamens für Sicherheit
|
|
||||||
function isValidHostname(hostname) {
|
|
||||||
// Längenprüfung
|
|
||||||
if (!hostname || hostname.length > 255) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erlaubte Hostnamen
|
|
||||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv4-Prüfung
|
|
||||||
const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
||||||
if (ipv4Regex.test(hostname)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hostname-Prüfung
|
|
||||||
const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
||||||
return hostnameRegex.test(hostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server starten
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
console.log(`MYP Frontend Debug-Server läuft auf http://localhost:${PORT}`);
|
|
||||||
});
|
|
@ -1,31 +0,0 @@
|
|||||||
#!/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
|
|
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
debug
|
|
||||||
}
|
|
||||||
|
|
||||||
# Hauptdomain für die Anwendung
|
|
||||||
m040tbaraspi001.de040.corpintra.net, m040tbaraspi001, localhost {
|
|
||||||
reverse_proxy myp-rp:3000
|
|
||||||
tls internal
|
|
||||||
|
|
||||||
# Erlaube HTTP -> HTTPS Redirects für OAuth
|
|
||||||
@oauth path /auth/login/callback*
|
|
||||||
handle @oauth {
|
|
||||||
header Cache-Control "no-cache"
|
|
||||||
reverse_proxy myp-rp:3000
|
|
||||||
}
|
|
||||||
|
|
||||||
# Allgemeine Header für Sicherheit und Caching
|
|
||||||
header {
|
|
||||||
# Sicherheitsheader
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
||||||
X-Content-Type-Options "nosniff"
|
|
||||||
X-Frame-Options "SAMEORIGIN"
|
|
||||||
Referrer-Policy "strict-origin-when-cross-origin"
|
|
||||||
|
|
||||||
# Cache-Control für statische Assets
|
|
||||||
@static {
|
|
||||||
path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
|
||||||
}
|
|
||||||
header @static Cache-Control "public, max-age=86400"
|
|
||||||
|
|
||||||
# Keine Caches für dynamische Inhalte
|
|
||||||
@dynamic {
|
|
||||||
not path *.js *.css *.png *.jpg *.svg *.ico *.woff *.woff2
|
|
||||||
}
|
|
||||||
header @dynamic Cache-Control "no-store, no-cache, must-revalidate"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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
|
|
||||||
environment:
|
|
||||||
- NEXT_PUBLIC_API_URL=http://192.168.0.105:5000
|
|
||||||
- OAUTH_CLIENT_ID=client_id
|
|
||||||
- OAUTH_CLIENT_SECRET=client_secret
|
|
||||||
env_file: "/srv/myp-env/github.env"
|
|
||||||
volumes:
|
|
||||||
- /srv/MYP-DB:/usr/src/app/db
|
|
||||||
restart: unless-stopped
|
|
||||||
# Füge Healthcheck hinzu für besseres Monitoring
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
@ -1,36 +0,0 @@
|
|||||||
#!/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"
|
|
@ -1,2 +0,0 @@
|
|||||||
caddy_2.8.tar.xz filter=lfs diff=lfs merge=lfs -text
|
|
||||||
myp-rp_latest.tar.xz filter=lfs diff=lfs merge=lfs -text
|
|
BIN
packages/reservation-platform/docker/images/caddy_2.8.tar.xz
(Stored with Git LFS)
BIN
packages/reservation-platform/docker/images/caddy_2.8.tar.xz
(Stored with Git LFS)
Binary file not shown.
BIN
packages/reservation-platform/docker/images/myp-rp_latest.tar.xz
(Stored with Git LFS)
BIN
packages/reservation-platform/docker/images/myp-rp_latest.tar.xz
(Stored with Git LFS)
Binary file not shown.
@ -1,68 +0,0 @@
|
|||||||
#!/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
|
|
@ -1,116 +0,0 @@
|
|||||||
# **Detaillierte Dokumentation des Admin-Dashboards**
|
|
||||||
|
|
||||||
In diesem Abschnitt werde ich die Funktionen und Nutzung des Admin-Dashboards genauer beschreiben, einschließlich der verschiedenen Module, Diagramme und deren Zweck.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **1. Überblick über das Admin-Dashboard**
|
|
||||||
|
|
||||||
Das Admin-Dashboard ist der zentrale Verwaltungsbereich für Administratoren. Es bietet Funktionen wie die Verwaltung von Druckern, Benutzern und Druckaufträgen sowie detaillierte Statistiken und Analysen.
|
|
||||||
|
|
||||||
### **1.1. Navigation**
|
|
||||||
Das Dashboard enthält ein Sidebar-Menü mit den folgenden Hauptbereichen:
|
|
||||||
1. **Dashboard:** Übersicht der wichtigsten Statistiken.
|
|
||||||
2. **Benutzer:** Verwaltung von Benutzerkonten.
|
|
||||||
3. **Drucker:** Hinzufügen, Bearbeiten und Verwalten von Druckern.
|
|
||||||
4. **Druckaufträge:** Einsicht in alle Druckaufträge und deren Status.
|
|
||||||
5. **Einstellungen:** Konfiguration der Anwendung.
|
|
||||||
6. **Über MYP:** Informationen über das Projekt und den Entwickler.
|
|
||||||
|
|
||||||
Die Sidebar wird in der Datei `src/app/admin/admin-sidebar.tsx` definiert und dynamisch basierend auf der aktuellen Seite hervorgehoben.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **2. Funktionen des Admin-Dashboards**
|
|
||||||
|
|
||||||
### **2.1. Benutzerverwaltung**
|
|
||||||
- **Datei:** `src/app/admin/users/page.tsx`
|
|
||||||
- **Beschreibung:** Ermöglicht das Anzeigen, Bearbeiten und Löschen von Benutzerkonten.
|
|
||||||
- **Funktionen:**
|
|
||||||
- Anzeige einer Liste aller registrierten Benutzer.
|
|
||||||
- Bearbeiten von Benutzerrollen (z. B. „admin“ oder „user“).
|
|
||||||
- Deaktivieren oder Löschen von Benutzerkonten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.2. Druckerverwaltung**
|
|
||||||
- **Datei:** `src/app/admin/printers/page.tsx`
|
|
||||||
- **Beschreibung:** Verwaltung der Drucker, einschließlich Hinzufügen, Bearbeiten und Deaktivieren.
|
|
||||||
- **Funktionen:**
|
|
||||||
- Statusanzeige der Drucker (aktiv/inaktiv).
|
|
||||||
- Hinzufügen neuer Drucker mit Namen und Beschreibung.
|
|
||||||
- Löschen oder Bearbeiten bestehender Drucker.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.3. Druckaufträge**
|
|
||||||
- **Datei:** `src/app/admin/jobs/page.tsx`
|
|
||||||
- **Beschreibung:** Übersicht aller Druckaufträge, einschließlich Details wie Startzeit, Dauer und Status.
|
|
||||||
- **Funktionen:**
|
|
||||||
- Filtern nach Benutzern, Druckern oder Status (abgeschlossen, abgebrochen).
|
|
||||||
- Anzeigen von Abbruchgründen und Fehlermeldungen.
|
|
||||||
- Sortieren nach Zeit oder Benutzer.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.4. Einstellungen**
|
|
||||||
- **Datei:** `src/app/admin/settings/page.tsx`
|
|
||||||
- **Beschreibung:** Konfigurationsseite für die Anwendung.
|
|
||||||
- **Funktionen:**
|
|
||||||
- Ändern von globalen Einstellungen wie Standardzeiten oder Fehlerrichtlinien.
|
|
||||||
- Download von Daten (z. B. Export der Druckhistorie).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **3. Statistiken und Diagramme**
|
|
||||||
|
|
||||||
Das Admin-Dashboard enthält interaktive Diagramme, die wichtige Statistiken visualisieren. Hier einige der zentralen Diagramme:
|
|
||||||
|
|
||||||
### **3.1. Abbruchgründe**
|
|
||||||
- **Datei:** `src/app/admin/charts/printer-error-chart.tsx`
|
|
||||||
- **Beschreibung:** Zeigt die Häufigkeit der Abbruchgründe für Druckaufträge in einem Balkendiagramm.
|
|
||||||
- **Nutzen:** Identifiziert häufige Probleme wie Materialmangel oder Düsenverstopfungen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **3.2. Fehlerraten**
|
|
||||||
- **Datei:** `src/app/admin/charts/printer-error-rate.tsx`
|
|
||||||
- **Beschreibung:** Zeigt die prozentuale Fehlerrate für jeden Drucker in einem Balkendiagramm.
|
|
||||||
- **Nutzen:** Ermöglicht die Überwachung und Identifizierung von problematischen Druckern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **3.3. Druckvolumen**
|
|
||||||
- **Datei:** `src/app/admin/charts/printer-volume.tsx`
|
|
||||||
- **Beschreibung:** Zeigt das Druckvolumen für heute, diese Woche und diesen Monat.
|
|
||||||
- **Nutzen:** Vergleich des Druckeroutputs über verschiedene Zeiträume.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **3.4. Prognostizierte Nutzung**
|
|
||||||
- **Datei:** `src/app/admin/charts/printer-forecast.tsx`
|
|
||||||
- **Beschreibung:** Ein Bereichsdiagramm zeigt die erwartete Druckernutzung pro Wochentag.
|
|
||||||
- **Nutzen:** Hilft bei der Planung von Wartungsarbeiten oder Ressourcenzuweisungen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **3.5. Druckerauslastung**
|
|
||||||
- **Datei:** `src/app/admin/charts/printer-utilization.tsx`
|
|
||||||
- **Beschreibung:** Zeigt die aktuelle Nutzung eines Druckers in Prozent in einem Kreisdiagramm.
|
|
||||||
- **Nutzen:** Überwacht die Auslastung und identifiziert ungenutzte Ressourcen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **4. Rollenbasierte Zugriffssteuerung**
|
|
||||||
|
|
||||||
Das Admin-Dashboard ist nur für Benutzer mit der Rolle „admin“ zugänglich. Nicht berechtigte Benutzer werden auf die Startseite umgeleitet. Die Zugriffssteuerung erfolgt durch folgende Logik:
|
|
||||||
- **Datei:** `src/app/admin/layout.tsx`
|
|
||||||
- **Funktion:** `validateRequest` prüft die Rolle des aktuellen Benutzers.
|
|
||||||
- **Umleitung:** Falls die Rolle unzureichend ist, wird der Benutzer automatisch umgeleitet:
|
|
||||||
```typescript
|
|
||||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
|
||||||
redirect("/");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Nächster Schritt: [=> API-Endpunkte und deren Nutzung](./API.md)
|
|
@ -1,79 +0,0 @@
|
|||||||
# **Technische Architektur und Codeaufbau**
|
|
||||||
|
|
||||||
In diesem Abschnitt erläutere ich die Architektur und Struktur des MYP-Projekts sowie die Funktionalitäten der zentralen Komponenten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **1. Technische Architektur**
|
|
||||||
|
|
||||||
### **1.1. Architekturübersicht**
|
|
||||||
MYP basiert auf einer modernen Webanwendungsarchitektur:
|
|
||||||
- **Frontend:** Entwickelt mit React und Next.js. Stellt die Benutzeroberfläche bereit.
|
|
||||||
- **Backend:** Nutzt Node.js und Drizzle ORM für die Datenbankinteraktion und Geschäftslogik.
|
|
||||||
- **Datenbank:** SQLite zur Speicherung von Nutzerdaten, Druckaufträgen und Druckerkonfigurationen.
|
|
||||||
- **Containerisierung:** Docker wird verwendet, um die Anwendung in isolierten Containern bereitzustellen.
|
|
||||||
- **Webserver:** Caddy dient als Reverse Proxy mit HTTPS-Unterstützung.
|
|
||||||
|
|
||||||
### **1.2. Modulübersicht**
|
|
||||||
- **Datenfluss:** Die Anwendung ist stark datengetrieben. API-Routen werden genutzt, um Daten zwischen Frontend und Backend auszutauschen.
|
|
||||||
- **Rollenbasierter Zugriff:** Über ein Berechtigungssystem können Administratoren und Benutzer unterschiedliche Funktionen nutzen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **2. Codeaufbau**
|
|
||||||
|
|
||||||
### **2.1. Ordnerstruktur**
|
|
||||||
Die Datei `repomix-output.txt` zeigt eine strukturierte Übersicht des Projekts. Nachfolgend einige wichtige Verzeichnisse:
|
|
||||||
|
|
||||||
| **Verzeichnis** | **Inhalt** |
|
|
||||||
|--------------------------|---------------------------------------------------------------------------|
|
|
||||||
| `src/app` | Next.js-Seiten und Komponenten für Benutzer und Admins. |
|
|
||||||
| `src/components` | Wiederverwendbare UI-Komponenten wie Karten, Diagramme, Buttons etc. |
|
|
||||||
| `src/server` | Backend-Logik, Authentifizierung und Datenbankinteraktionen. |
|
|
||||||
| `src/utils` | Hilfsfunktionen für Analysen, Validierungen und Datenbankzugriffe. |
|
|
||||||
| `drizzle` | Datenbank-Migrationsdateien und Metadaten. |
|
|
||||||
| `docker` | Docker-Konfigurations- und Bereitstellungsskripte. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.2. Hauptdateien**
|
|
||||||
#### **Frontend**
|
|
||||||
- **`src/app/page.tsx`:** Startseite der Anwendung.
|
|
||||||
- **`src/app/admin/`:** Admin-spezifische Seiten, z. B. Druckerverwaltung oder Fehlerstatistiken.
|
|
||||||
- **`src/components/ui/`:** UI-Komponenten wie Dialoge, Formulare und Tabellen.
|
|
||||||
|
|
||||||
#### **Backend**
|
|
||||||
- **`src/server/auth/`:** Authentifizierung und Benutzerrollenmanagement.
|
|
||||||
- **`src/server/actions/`:** Funktionen zur Interaktion mit Druckaufträgen und Druckern.
|
|
||||||
- **`src/utils/`:** Analyse und Verarbeitung von Druckdaten (z. B. Fehlerquoten und Auslastung).
|
|
||||||
|
|
||||||
#### **Datenbank**
|
|
||||||
- **`drizzle/0000_overjoyed_strong_guy.sql`:** SQLite-Datenbankschema mit Tabellen für Drucker, Benutzer und Druckaufträge.
|
|
||||||
- **`drizzle.meta/`:** Metadaten zur Datenbankmigration.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.3. Datenbankschema**
|
|
||||||
Das Schema enthält vier Haupttabellen:
|
|
||||||
1. **`user`:** Speichert Benutzerinformationen, einschließlich Rollen und E-Mail-Adressen.
|
|
||||||
2. **`printer`:** Beschreibt die Drucker, ihren Status und ihre Eigenschaften.
|
|
||||||
3. **`printJob`:** Zeichnet Druckaufträge auf, einschließlich Startzeit, Dauer und Abbruchgrund.
|
|
||||||
4. **`session`:** Verwaltert Benutzer-Sitzungen und Ablaufzeiten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **3. Wichtige Funktionen**
|
|
||||||
|
|
||||||
### **3.1. Authentifizierung**
|
|
||||||
Das System nutzt OAuth zur Anmeldung. Benutzerrollen werden in der Tabelle `user` gespeichert und im Backend überprüft.
|
|
||||||
|
|
||||||
### **3.2. Statistiken**
|
|
||||||
- **Fehlerrate:** Berechnet die Häufigkeit von Abbrüchen für jeden Drucker.
|
|
||||||
- **Auslastung:** Prozentuale Nutzung der Drucker, basierend auf geplanten und abgeschlossenen Druckaufträgen.
|
|
||||||
- **Prognosen:** Verwenden historische Daten, um zukünftige Drucknutzungen vorherzusagen.
|
|
||||||
|
|
||||||
### **3.3. API-Endpunkte**
|
|
||||||
- **`src/app/api/printers/`:** Zugriff auf Druckerkonfigurationsdaten.
|
|
||||||
- **`src/app/api/job/[jobId]/`:** Verwaltung einzelner Druckaufträge.
|
|
||||||
|
|
||||||
Nächster Schritt: [=> Datenbank und Analytik-Funktionen](./Datenbank.md)
|
|
@ -1,150 +0,0 @@
|
|||||||
# **Bereitstellungsdetails und Best Practices**
|
|
||||||
|
|
||||||
In diesem Abschnitt erläutere ich, wie das MYP-Projekt auf einem Server bereitgestellt wird, sowie empfohlene Praktiken zur Verwaltung und Optimierung des Systems.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **1. Bereitstellungsschritte**
|
|
||||||
|
|
||||||
### **1.1. Voraussetzungen**
|
|
||||||
- **Server:** Raspberry Pi mit installiertem Raspbian Lite.
|
|
||||||
- **Docker:** Docker und Docker Compose müssen vorab installiert sein.
|
|
||||||
- **Netzwerk:** Der Server muss über eine statische IP-Adresse oder einen DNS-Namen erreichbar sein.
|
|
||||||
|
|
||||||
### **1.2. Vorbereitung**
|
|
||||||
#### **1.2.1. Docker-Images erstellen und speichern**
|
|
||||||
Führen Sie die folgenden Schritte auf dem Entwicklungssystem aus:
|
|
||||||
1. **Images erstellen:**
|
|
||||||
```bash
|
|
||||||
bash docker/build.sh
|
|
||||||
```
|
|
||||||
2. **Images exportieren und komprimieren:**
|
|
||||||
```bash
|
|
||||||
bash docker/save.sh <image-name>
|
|
||||||
```
|
|
||||||
Dies speichert die Docker-Images im Verzeichnis `docker/images/`.
|
|
||||||
|
|
||||||
#### **1.2.2. Übertragung auf den Server**
|
|
||||||
Kopieren Sie die erzeugten `.tar.xz`-Dateien auf den Raspberry Pi:
|
|
||||||
```bash
|
|
||||||
scp docker/images/*.tar.xz <username>@<server-ip>:/path/to/destination/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **1.3. Images auf dem Server laden**
|
|
||||||
Loggen Sie sich auf dem Server ein und laden Sie die Docker-Images:
|
|
||||||
```bash
|
|
||||||
docker load -i /path/to/destination/<image-name>.tar.xz
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **1.4. Starten der Anwendung**
|
|
||||||
Führen Sie das Bereitstellungsskript aus:
|
|
||||||
```bash
|
|
||||||
bash docker/deploy.sh
|
|
||||||
```
|
|
||||||
Dieses Skript:
|
|
||||||
- Startet die Docker-Container mithilfe von `docker compose`.
|
|
||||||
- Verbindet den Reverse Proxy (Caddy) mit der Anwendung.
|
|
||||||
|
|
||||||
Die Anwendung sollte unter `http://<server-ip>` oder der konfigurierten Domain erreichbar sein.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **2. Best Practices**
|
|
||||||
|
|
||||||
### **2.1. Sicherheit**
|
|
||||||
1. **Umgebungsvariablen schützen:**
|
|
||||||
- Stellen Sie sicher, dass die Datei `.env` nicht versehentlich in ein öffentliches Repository hochgeladen wird.
|
|
||||||
- Verwenden Sie geeignete Zugriffsrechte:
|
|
||||||
```bash
|
|
||||||
chmod 600 .env
|
|
||||||
```
|
|
||||||
2. **HTTPS aktivieren:**
|
|
||||||
- Der Caddy-Webserver unterstützt automatisch HTTPS. Stellen Sie sicher, dass eine gültige Domain konfiguriert ist.
|
|
||||||
|
|
||||||
3. **Zugriffsrechte beschränken:**
|
|
||||||
- Verwenden Sie Benutzerrollen („admin“, „guest“), um den Zugriff auf kritische Funktionen zu steuern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.2. Performance**
|
|
||||||
1. **Docker-Container optimieren:**
|
|
||||||
- Reduzieren Sie die Größe der Docker-Images, indem Sie unnötige Dateien in `.dockerignore` ausschließen.
|
|
||||||
|
|
||||||
2. **Datenbankwartung:**
|
|
||||||
- Führen Sie regelmäßige Backups der SQLite-Datenbank durch:
|
|
||||||
```bash
|
|
||||||
cp db/sqlite.db /path/to/backup/location/
|
|
||||||
```
|
|
||||||
- Optimieren Sie die Datenbank regelmäßig:
|
|
||||||
```sql
|
|
||||||
VACUUM;
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Skalierung:**
|
|
||||||
- Bei hoher Last kann die Anwendung mit Kubernetes oder einer Cloud-Lösung (z. B. AWS oder Azure) skaliert werden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.3. Fehlerbehebung**
|
|
||||||
1. **Logs überprüfen:**
|
|
||||||
- Docker-Logs können wichtige Debug-Informationen liefern:
|
|
||||||
```bash
|
|
||||||
docker logs <container-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Health Checks:**
|
|
||||||
- Integrieren Sie Health Checks in die Docker Compose-Datei, um sicherzustellen, dass die Dienste korrekt laufen.
|
|
||||||
|
|
||||||
3. **Fehlerhafte Drucker deaktivieren:**
|
|
||||||
- Deaktivieren Sie Drucker mit einer hohen Fehlerrate über das Admin-Dashboard, um die Benutzererfahrung zu verbessern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.4. Updates**
|
|
||||||
1. **Neue Funktionen hinzufügen:**
|
|
||||||
- Aktualisieren Sie die Anwendung und erstellen Sie neue Docker-Images:
|
|
||||||
```bash
|
|
||||||
git pull origin main
|
|
||||||
bash docker/build.sh
|
|
||||||
```
|
|
||||||
- Stellen Sie die aktualisierten Images bereit:
|
|
||||||
```bash
|
|
||||||
bash docker/deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Datenbankmigrationen:**
|
|
||||||
- Führen Sie neue Migrationsskripte mit folgendem Befehl aus:
|
|
||||||
```bash
|
|
||||||
pnpm run db:migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **3. Backup und Wiederherstellung**
|
|
||||||
|
|
||||||
### **3.1. Backups erstellen**
|
|
||||||
Sichern Sie wichtige Dateien und Datenbanken regelmäßig:
|
|
||||||
- **SQLite-Datenbank:**
|
|
||||||
```bash
|
|
||||||
cp db/sqlite.db /backup/location/sqlite-$(date +%F).db
|
|
||||||
```
|
|
||||||
- **Docker-Images:**
|
|
||||||
```bash
|
|
||||||
docker save myp-rp:latest | gzip > /backup/location/myp-rp-$(date +%F).tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
### **3.2. Wiederherstellung**
|
|
||||||
- **Datenbank wiederherstellen:**
|
|
||||||
```bash
|
|
||||||
cp /backup/location/sqlite-<date>.db db/sqlite.db
|
|
||||||
```
|
|
||||||
- **Docker-Images importieren:**
|
|
||||||
```bash
|
|
||||||
docker load < /backup/location/myp-rp-<date>.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
Nächster Schritt: [=> Admin-Dashboard](./Admin-Dashboard.md)
|
|
@ -1,153 +0,0 @@
|
|||||||
# **Datenbank und Analytik-Funktionen**
|
|
||||||
|
|
||||||
Dieser Abschnitt konzentriert sich auf die Struktur der Datenbank sowie die Analyse- und Prognosefunktionen, die im Projekt verwendet werden.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **1. Datenbankstruktur**
|
|
||||||
|
|
||||||
Das Datenbankschema wurde mit **Drizzle ORM** definiert und basiert auf SQLite. Die wichtigsten Tabellen und ihre Zwecke sind:
|
|
||||||
|
|
||||||
### **1.1. Tabellenübersicht**
|
|
||||||
|
|
||||||
#### **`user`**
|
|
||||||
- Speichert Benutzerinformationen.
|
|
||||||
- Enthält Rollen wie „admin“ oder „guest“ zur Verwaltung von Berechtigungen.
|
|
||||||
|
|
||||||
| **Feld** | **Typ** | **Beschreibung** |
|
|
||||||
|-------------------|------------|-------------------------------------------|
|
|
||||||
| `id` | `text` | Eindeutige ID des Benutzers. |
|
|
||||||
| `github_id` | `integer` | ID des Benutzers aus dem OAuth-Dienst. |
|
|
||||||
| `name` | `text` | Benutzername. |
|
|
||||||
| `displayName` | `text` | Angezeigter Name. |
|
|
||||||
| `email` | `text` | E-Mail-Adresse. |
|
|
||||||
| `role` | `text` | Benutzerrolle, Standardwert: „guest“. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **`printer`**
|
|
||||||
- Beschreibt verfügbare Drucker und deren Status.
|
|
||||||
|
|
||||||
| **Feld** | **Typ** | **Beschreibung** |
|
|
||||||
|-------------------|------------|-------------------------------------------|
|
|
||||||
| `id` | `text` | Eindeutige Drucker-ID. |
|
|
||||||
| `name` | `text` | Name des Druckers. |
|
|
||||||
| `description` | `text` | Beschreibung oder Spezifikationen. |
|
|
||||||
| `status` | `integer` | Betriebsstatus (0 = inaktiv, 1 = aktiv). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **`printJob`**
|
|
||||||
- Speichert Informationen zu Druckaufträgen.
|
|
||||||
|
|
||||||
| **Feld** | **Typ** | **Beschreibung** |
|
|
||||||
|-----------------------|---------------|-------------------------------------------------------|
|
|
||||||
| `id` | `text` | Eindeutige Auftrags-ID. |
|
|
||||||
| `printerId` | `text` | Verweis auf die ID des Druckers. |
|
|
||||||
| `userId` | `text` | Verweis auf die ID des Benutzers. |
|
|
||||||
| `startAt` | `integer` | Startzeit des Druckauftrags (Unix-Timestamp). |
|
|
||||||
| `durationInMinutes` | `integer` | Dauer des Druckauftrags in Minuten. |
|
|
||||||
| `comments` | `text` | Zusätzliche Kommentare. |
|
|
||||||
| `aborted` | `integer` | 1 = Abgebrochen, 0 = Erfolgreich abgeschlossen. |
|
|
||||||
| `abortReason` | `text` | Grund für den Abbruch (falls zutreffend). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### **`session`**
|
|
||||||
- Verwaltert Benutzer-Sitzungen und Ablaufzeiten.
|
|
||||||
|
|
||||||
| **Feld** | **Typ** | **Beschreibung** |
|
|
||||||
|-------------------|------------|-------------------------------------------|
|
|
||||||
| `id` | `text` | Eindeutige Sitzungs-ID. |
|
|
||||||
| `user_id` | `text` | Verweis auf die ID des Benutzers. |
|
|
||||||
| `expires_at` | `integer` | Zeitpunkt, wann die Sitzung abläuft. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **1.2. Relationen**
|
|
||||||
- `printer` → `printJob`: Druckaufträge sind an spezifische Drucker gebunden.
|
|
||||||
- `user` → `printJob`: Druckaufträge werden Benutzern zugewiesen.
|
|
||||||
- `user` → `session`: Sitzungen verknüpfen Benutzer mit Login-Details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **2. Analytik-Funktionen**
|
|
||||||
|
|
||||||
Das Projekt bietet verschiedene Analytik- und Prognosetools, um die Druckernutzung und Fehler zu überwachen.
|
|
||||||
|
|
||||||
### **2.1. Fehlerratenanalyse**
|
|
||||||
- Funktion: `calculatePrinterErrorRate` (in `src/utils/analytics/error-rate.ts`).
|
|
||||||
- Berechnet die prozentuale Fehlerrate für jeden Drucker basierend auf abgebrochenen Aufträgen.
|
|
||||||
|
|
||||||
Beispielausgabe:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "name": "Drucker 1", "errorRate": 5.2 },
|
|
||||||
{ "name": "Drucker 2", "errorRate": 3.7 }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.2. Abbruchgründe**
|
|
||||||
- Funktion: `calculateAbortReasonsCount` (in `src/utils/analytics/errors.ts`).
|
|
||||||
- Zählt die Häufigkeit der Abbruchgründe aus der Tabelle `printJob`.
|
|
||||||
|
|
||||||
Beispielausgabe:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "abortReason": "Materialmangel", "count": 10 },
|
|
||||||
{ "abortReason": "Düsenverstopfung", "count": 7 }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.3. Nutzung und Prognosen**
|
|
||||||
#### Nutzung:
|
|
||||||
- Funktion: `calculatePrinterUtilization` (in `src/utils/analytics/utilization.ts`).
|
|
||||||
- Berechnet die Nutzung der Drucker in Prozent.
|
|
||||||
|
|
||||||
Beispielausgabe:
|
|
||||||
```json
|
|
||||||
{ "printerId": "1", "utilizationPercentage": 85 }
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Prognosen:
|
|
||||||
- Funktion: `forecastPrinterUsage` (in `src/utils/analytics/forecast.ts`).
|
|
||||||
- Nutzt historische Daten, um die erwartete Druckernutzung für kommende Tage/Wochen zu schätzen.
|
|
||||||
|
|
||||||
Beispielausgabe:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "day": 1, "usageMinutes": 300 },
|
|
||||||
{ "day": 2, "usageMinutes": 200 }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### **2.4. Druckvolumen**
|
|
||||||
- Funktion: `calculatePrintVolumes` (in `src/utils/analytics/volume.ts`).
|
|
||||||
- Vergleicht die Anzahl der abgeschlossenen Druckaufträge für heute, diese Woche und diesen Monat.
|
|
||||||
|
|
||||||
Beispielausgabe:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"today": 15,
|
|
||||||
"thisWeek": 90,
|
|
||||||
"thisMonth": 300
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **3. Datenbankinitialisierung**
|
|
||||||
Die Datenbank wird über Skripte in der `package.json` initialisiert:
|
|
||||||
```bash
|
|
||||||
pnpm run db:clean # Datenbank und Migrationsordner löschen
|
|
||||||
pnpm run db:generate # Neues Schema generieren
|
|
||||||
pnpm run db:migrate # Migrationsskripte ausführen
|
|
||||||
```
|
|
||||||
|
|
||||||
Nächster Schritt: [=> Bereitstellungsdetails und Best Practices](./Bereitstellungsdetails.md)
|
|
@ -1,93 +0,0 @@
|
|||||||
# **Installation und Einrichtung**
|
|
||||||
|
|
||||||
In diesem Abschnitt wird beschrieben, wie die MYP-Anwendung installiert und eingerichtet wird. Diese Schritte umfassen die Vorbereitung der Umgebung, das Konfigurieren der notwendigen Dienste und die Bereitstellung des Projekts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Voraussetzungen**
|
|
||||||
### **Hardware und Software**
|
|
||||||
- **Raspberry Pi:** Die Anwendung ist für den Einsatz auf einem Raspberry Pi optimiert, auf dem Raspbian Lite installiert sein sollte.
|
|
||||||
- **Docker:** Docker und Docker Compose müssen installiert sein.
|
|
||||||
- **Netzwerkzugriff:** Der Raspberry Pi muss im Netzwerk erreichbar sein.
|
|
||||||
|
|
||||||
### **Abhängigkeiten**
|
|
||||||
- Node.js (mindestens Version 20)
|
|
||||||
- PNPM (Paketmanager)
|
|
||||||
- SQLite (für lokale Datenbankverwaltung)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Schritte zur Einrichtung**
|
|
||||||
|
|
||||||
### **1. Repository klonen**
|
|
||||||
Klonen Sie das Repository auf Ihr System:
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd <repository-ordner>
|
|
||||||
```
|
|
||||||
|
|
||||||
### **2. Konfiguration der Umgebungsvariablen**
|
|
||||||
Passen Sie die Datei `.env.example` an und benennen Sie sie in `.env` um:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
Erforderliche Variablen:
|
|
||||||
- `OAUTH_CLIENT_ID`: Client-ID für die OAuth-Authentifizierung
|
|
||||||
- `OAUTH_CLIENT_SECRET`: Geheimnis für die OAuth-Authentifizierung
|
|
||||||
|
|
||||||
### **3. Docker-Container erstellen**
|
|
||||||
Führen Sie das Skript `build.sh` aus, um Docker-Images zu erstellen:
|
|
||||||
```bash
|
|
||||||
bash docker/build.sh
|
|
||||||
```
|
|
||||||
Dies erstellt die notwendigen Docker-Images, einschließlich der Anwendung und eines Caddy-Webservers.
|
|
||||||
|
|
||||||
### **4. Docker-Images speichern**
|
|
||||||
Speichern Sie die Images in komprimierter Form, um sie auf anderen Geräten bereitzustellen:
|
|
||||||
```bash
|
|
||||||
bash docker/save.sh <image-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
### **5. Bereitstellung**
|
|
||||||
Kopieren Sie die Docker-Images auf den Zielserver (z. B. Raspberry Pi) und führen Sie `deploy.sh` aus:
|
|
||||||
```bash
|
|
||||||
scp docker/images/*.tar.xz <ziel-server>:/path/to/deployment/
|
|
||||||
bash docker/deploy.sh
|
|
||||||
```
|
|
||||||
Das Skript führt die Docker Compose-Konfiguration aus und startet die Anwendung.
|
|
||||||
|
|
||||||
### **(Optional: 6. Admin-User anlegen)**
|
|
||||||
|
|
||||||
Um einen Admin-User anzulegen, muss zuerst das Container-Image gestartet werden. Anschließend meldet man sich mittels
|
|
||||||
der GitHub-Authentifizierung bei der Anwendung an.
|
|
||||||
|
|
||||||
Der nun in der Datenbank angelegte User hat die Rolle `guest`. Über das CLI muss man nun in die SQLite-Datenbank (die Datenbank sollte außerhalb des Container-Images liegen) wechseln und
|
|
||||||
den User updaten.
|
|
||||||
|
|
||||||
|
|
||||||
#### SQL-Befehl, um den User zu updaten:
|
|
||||||
```bash
|
|
||||||
sqlite3 db.sqlite3
|
|
||||||
UPDATE users SET role = 'admin' WHERE id = <user-id>;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Start der Anwendung**
|
|
||||||
Sobald die Docker-Container laufen, ist die Anwendung unter der angegebenen Domain oder IP-Adresse erreichbar. Standardmäßig verwendet der Caddy-Webserver Port 80 (HTTP) und 443 (HTTPS).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Optional: Entwicklungsmodus**
|
|
||||||
Für lokale Tests können Sie die Anwendung ohne Docker starten:
|
|
||||||
1. Installieren Sie Abhängigkeiten:
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
2. Starten Sie den Entwicklungsserver:
|
|
||||||
```bash
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
Die Anwendung ist dann unter `http://localhost:3000` verfügbar.
|
|
||||||
|
|
||||||
Nächster Schritt: [=> Nutzung](./Nutzung.md)
|
|
@ -1,75 +0,0 @@
|
|||||||
# **Features und Nutzung der Anwendung**
|
|
||||||
|
|
||||||
In diesem Abschnitt beschreibe ich die Hauptfunktionen von MYP (Manage Your Printer) und gebe Anweisungen zur Nutzung der verschiedenen Module.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **1. Hauptfunktionen**
|
|
||||||
|
|
||||||
### **1.1. Druckerreservierung**
|
|
||||||
- Nutzer können Drucker für einen definierten Zeitraum reservieren.
|
|
||||||
- Konflikte bei Reservierungen werden durch ein Echtzeit-Überprüfungssystem verhindert.
|
|
||||||
|
|
||||||
### **1.2. Fehler- und Auslastungsanalyse**
|
|
||||||
- Darstellung von Druckfehlern nach Kategorien und Häufigkeiten.
|
|
||||||
- Übersicht der aktuellen und historischen Druckernutzung.
|
|
||||||
- Diagramme zur Fehlerrate, Nutzung und Druckvolumen.
|
|
||||||
|
|
||||||
### **1.3. Admin-Dashboard**
|
|
||||||
- Verwaltung von Druckern, Nutzern und Druckaufträgen.
|
|
||||||
- Überblick über alle Abbruchgründe und Druckfehler.
|
|
||||||
- Zugriff auf erweiterte Statistiken und Prognosen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **2. Nutzung der Anwendung**
|
|
||||||
|
|
||||||
### **2.1. Login und Authentifizierung**
|
|
||||||
- Die Anwendung unterstützt OAuth-basierte Authentifizierung.
|
|
||||||
- Nutzer müssen sich mit einem gültigen Konto anmelden, um Zugriff auf die Funktionen zu erhalten.
|
|
||||||
|
|
||||||
### **2.2. Dashboard**
|
|
||||||
- Nach dem Login gelangen die Nutzer auf das Dashboard, das einen Überblick über die aktuelle Druckernutzung bietet.
|
|
||||||
- Administratoren haben Zugriff auf zusätzliche Menüpunkte, wie z. B. Benutzerverwaltung.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **3. Admin-Funktionen**
|
|
||||||
|
|
||||||
### **3.1. Druckerverwaltung**
|
|
||||||
- Administratoren können Drucker hinzufügen, bearbeiten oder löschen.
|
|
||||||
- Status eines Druckers (z. B. „in Betrieb“, „außer Betrieb“) kann angepasst werden.
|
|
||||||
|
|
||||||
### **3.2. Nutzerverwaltung**
|
|
||||||
- Verwalten von Benutzerkonten, einschließlich Rollen (z. B. „Admin“ oder „User“).
|
|
||||||
- Benutzer können aktiviert oder deaktiviert werden.
|
|
||||||
|
|
||||||
### **3.3. Statistiken und Berichte**
|
|
||||||
- Diagramme wie:
|
|
||||||
- **Abbruchgründe:** Zeigt häufige Fehlerursachen.
|
|
||||||
- **Fehlerrate:** Prozentuale Fehlerquote der Drucker.
|
|
||||||
- **Nutzung:** Prognosen für die Druckernutzung pro Wochentag.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **4. Diagramme und Visualisierungen**
|
|
||||||
|
|
||||||
### **4.1. Abbruchgründe**
|
|
||||||
- Ein Säulendiagramm zeigt die Häufigkeiten der Fehlerursachen.
|
|
||||||
- Nutzt Echtzeit-Daten aus der Druckhistorie.
|
|
||||||
|
|
||||||
### **4.2. Prognostizierte Nutzung**
|
|
||||||
- Ein Liniendiagramm zeigt die erwartete Druckernutzung pro Tag.
|
|
||||||
- Hilft bei der Planung von Wartungszeiten.
|
|
||||||
|
|
||||||
### **4.3. Druckvolumen**
|
|
||||||
- Balkendiagramme vergleichen Druckaufträge heute, diese Woche und diesen Monat.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **5. Interaktive Komponenten**
|
|
||||||
- **Benachrichtigungen:** Informieren über Druckaufträge, Fehler oder Systemereignisse.
|
|
||||||
- **Filter und Suchfunktionen:** Erleichtern das Auffinden von Druckern oder Druckaufträgen.
|
|
||||||
- **Rollenbasierter Zugriff:** Funktionen sind je nach Benutzerrolle eingeschränkt.
|
|
||||||
|
|
||||||
Nächster Schritt: [=> Technische Architektur und Codeaufbau](./Architektur.md)
|
|
@ -1,37 +0,0 @@
|
|||||||
# **Einleitung**
|
|
||||||
|
|
||||||
> Information: Die Dokumenation wurde mit generativer AI erstellt und kann fehlerhaft sein. Im Zweifel bitte die Quellcode-Dateien anschauen oder die Entwickler kontaktieren.
|
|
||||||
|
|
||||||
## **Projektbeschreibung**
|
|
||||||
MYP (Manage Your Printer) ist eine Webanwendung zur Verwaltung und Reservierung von 3D-Druckern. Das Projekt wurde als Abschlussarbeit im Rahmen der Fachinformatiker-Ausbildung mit Schwerpunkt Daten- und Prozessanalyse entwickelt und dient als Plattform zur einfachen Koordination und Überwachung von Druckressourcen. Es wurde speziell für die Technische Berufsausbildung des Mercedes-Benz Werkes in Berlin-Marienfelde erstellt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Hauptmerkmale**
|
|
||||||
- **Druckerreservierungen:** Nutzer können 3D-Drucker in definierten Zeitfenstern reservieren.
|
|
||||||
- **Fehleranalyse:** Statistiken über Druckfehler und Abbruchgründe werden visuell dargestellt.
|
|
||||||
- **Druckauslastung:** Echtzeit-Daten über die Nutzung der Drucker.
|
|
||||||
- **Admin-Dashboard:** Übersichtliche Verwaltung und Konfiguration von Druckern, Benutzern und Druckaufträgen.
|
|
||||||
- **Datenbankintegration:** Alle Daten werden in einer SQLite-Datenbank gespeichert und verwaltet.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Technologien**
|
|
||||||
- **Frontend:** React, Next.js, TailwindCSS
|
|
||||||
- **Backend:** Node.js, Drizzle ORM
|
|
||||||
- **Datenbank:** SQLite
|
|
||||||
- **Deployment:** Docker und Raspberry Pi
|
|
||||||
- **Zusätzliche Bibliotheken:** recharts für Diagramme, Faker.js für Testdaten, sowie diverse Radix-UI-Komponenten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## **Dateistruktur**
|
|
||||||
Die Repository-Dateien sind in logische Abschnitte unterteilt:
|
|
||||||
1. **Docker-Konfigurationen** (`docker/`) - Skripte und Konfigurationsdateien für die Bereitstellung.
|
|
||||||
2. **Frontend-Komponenten** (`src/app/`) - Weboberfläche und deren Funktionalitäten.
|
|
||||||
3. **Backend-Funktionen** (`src/server/`) - Datenbankinteraktionen und Authentifizierungslogik.
|
|
||||||
4. **Utils und Helferfunktionen** (`src/utils/`) - Wiederverwendbare Dienste und Hilfsmethoden.
|
|
||||||
5. **Datenbank-Skripte** (`drizzle/`) - Datenbankschemas und Migrationsdateien.
|
|
||||||
|
|
||||||
|
|
||||||
Nächster Schritt: [=> Installation](./Installation.md)
|
|
@ -1,12 +0,0 @@
|
|||||||
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",
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,35 +0,0 @@
|
|||||||
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'
|
|
||||||
);
|
|
@ -1,241 +0,0 @@
|
|||||||
{
|
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "6",
|
|
||||||
"when": 1715416514336,
|
|
||||||
"tag": "0000_overjoyed_strong_guy",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
/** @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;
|
|
@ -1,83 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "myp-rp",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"private": true,
|
|
||||||
"packageManager": "pnpm@9.12.1",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "node update-package.js && 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"
|
|
||||||
}
|
|
||||||
}
|
|
5707
packages/reservation-platform/pnpm-lock.yaml
generated
5707
packages/reservation-platform/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
@ -1 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 629 B |
File diff suppressed because it is too large
Load Diff
@ -1,367 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
@ -1,82 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Skript zum Setzen der Backend-URL in der Frontend-Konfiguration
|
|
||||||
# Verwendet für die Verbindung zum Backend-Server unter 192.168.0.105:5000
|
|
||||||
|
|
||||||
# Farbcodes für Ausgabe
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[0;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Funktion zur Ausgabe mit Zeitstempel
|
|
||||||
log() {
|
|
||||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
error_log() {
|
|
||||||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] FEHLER:${NC} $1" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
# Definiere Variablen
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
ENV_FILE="$SCRIPT_DIR/.env.local"
|
|
||||||
DEFAULT_BACKEND_URL="http://192.168.0.105:5000"
|
|
||||||
|
|
||||||
# Falls übergebene Parameter vorhanden sind, Backend-URL anpassen
|
|
||||||
if [ -n "$1" ]; then
|
|
||||||
BACKEND_URL="$1"
|
|
||||||
log "Verwende übergebene Backend-URL: ${BACKEND_URL}"
|
|
||||||
else
|
|
||||||
BACKEND_URL="$DEFAULT_BACKEND_URL"
|
|
||||||
log "Verwende Standard-Backend-URL: ${BACKEND_URL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Bestimme den Hostnamen für OAuth
|
|
||||||
HOSTNAME=$(hostname)
|
|
||||||
if [[ "$HOSTNAME" == *"m040tbaraspi001"* ]] || [[ "$HOSTNAME" == *"corpintra"* ]]; then
|
|
||||||
FRONTEND_HOSTNAME="m040tbaraspi001.de040.corpintra.net"
|
|
||||||
OAUTH_URL="http://m040tbaraspi001.de040.corpintra.net/auth/login/callback"
|
|
||||||
log "Erkannt: Unternehmens-Hostname: $FRONTEND_HOSTNAME"
|
|
||||||
else
|
|
||||||
FRONTEND_HOSTNAME="$HOSTNAME"
|
|
||||||
OAUTH_URL="http://$HOSTNAME:3000/auth/login/callback"
|
|
||||||
log "Lokaler Hostname: $FRONTEND_HOSTNAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Erstelle .env.local Datei mit Backend-URL
|
|
||||||
log "${YELLOW}Erstelle .env.local Datei...${NC}"
|
|
||||||
cat > "$ENV_FILE" << EOL
|
|
||||||
# Backend API Konfiguration
|
|
||||||
NEXT_PUBLIC_API_URL=${BACKEND_URL}
|
|
||||||
|
|
||||||
# Frontend-URL für OAuth Callback
|
|
||||||
NEXT_PUBLIC_FRONTEND_URL=http://${FRONTEND_HOSTNAME}
|
|
||||||
|
|
||||||
# Explizite OAuth Callback URL für GitHub
|
|
||||||
NEXT_PUBLIC_OAUTH_CALLBACK_URL=${OAUTH_URL}
|
|
||||||
|
|
||||||
# OAuth Konfiguration (falls nötig)
|
|
||||||
OAUTH_CLIENT_ID=client_id
|
|
||||||
OAUTH_CLIENT_SECRET=client_secret
|
|
||||||
EOL
|
|
||||||
|
|
||||||
# Überprüfe, ob die Datei erstellt wurde
|
|
||||||
if [ -f "$ENV_FILE" ]; then
|
|
||||||
log "${GREEN}Erfolgreich .env.local Datei mit Backend-URL erstellt: ${BACKEND_URL}${NC}"
|
|
||||||
else
|
|
||||||
error_log "Konnte .env.local Datei nicht erstellen."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Hinweis für Docker-Installation
|
|
||||||
log "${YELLOW}WICHTIG: Wenn Sie Docker verwenden, stellen Sie sicher, dass Sie die Umgebungsvariable setzen:${NC}"
|
|
||||||
log "NEXT_PUBLIC_API_URL=${BACKEND_URL}"
|
|
||||||
log ""
|
|
||||||
log "${GREEN}Backend-URL wurde erfolgreich konfiguriert. Nach einem Neustart der Anwendung sollte die Verbindung hergestellt werden.${NC}"
|
|
||||||
|
|
||||||
# Berechtigungen setzen
|
|
||||||
chmod 600 "$ENV_FILE"
|
|
||||||
|
|
||||||
exit 0
|
|
@ -1,32 +0,0 @@
|
|||||||
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 — 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>
|
|
||||||
© 2024{" "}
|
|
||||||
<a href="https://linkedin.com/in/torben-haack" target="_blank" rel="noreferrer">
|
|
||||||
Torben Haack
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
|
||||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
export const description = "Ein Säulendiagramm zur Darstellung der Abbruchgründe und ihrer Häufigkeit";
|
|
||||||
|
|
||||||
interface AbortReasonCountChartProps {
|
|
||||||
abortReasonCount: {
|
|
||||||
abortReason: string;
|
|
||||||
count: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
abortReason: {
|
|
||||||
label: "Abbruchgrund",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export function AbortReasonCountChart({ abortReasonCount }: AbortReasonCountChartProps) {
|
|
||||||
// Transform data to fit the chart structure
|
|
||||||
const chartData = abortReasonCount.map((reason) => ({
|
|
||||||
abortReason: reason.abortReason,
|
|
||||||
count: reason.count,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Abbruchgründe</CardTitle>
|
|
||||||
<CardDescription>Häufigkeit der Abbruchgründe für Druckaufträge</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer config={chartConfig}>
|
|
||||||
<BarChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={chartData}
|
|
||||||
margin={{
|
|
||||||
top: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="abortReason"
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => value}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={(value) => `${value}`} />
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Bar dataKey="count" fill="hsl(var(--chart-1))" radius={8}>
|
|
||||||
<LabelList
|
|
||||||
position="top"
|
|
||||||
offset={12}
|
|
||||||
className="fill-foreground"
|
|
||||||
fontSize={12}
|
|
||||||
formatter={(value: number) => `${value}`}
|
|
||||||
/>
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
|
||||||
import type { PrinterErrorRate } from "@/utils/analytics/error-rate";
|
|
||||||
|
|
||||||
export const description = "Ein Säulendiagramm zur Darstellung der Fehlerrate";
|
|
||||||
|
|
||||||
interface PrinterErrorRateChartProps {
|
|
||||||
printerErrorRate: PrinterErrorRate[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
errorRate: {
|
|
||||||
label: "Fehlerrate",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export function PrinterErrorRateChart({ printerErrorRate }: PrinterErrorRateChartProps) {
|
|
||||||
// Transform data to fit the chart structure
|
|
||||||
const chartData = printerErrorRate.map((printer) => ({
|
|
||||||
printer: printer.name,
|
|
||||||
errorRate: printer.errorRate,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Fehlerrate</CardTitle>
|
|
||||||
<CardDescription>Fehlerrate der Drucker in Prozent</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer config={chartConfig}>
|
|
||||||
<BarChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={chartData}
|
|
||||||
margin={{
|
|
||||||
top: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="printer"
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => value}
|
|
||||||
/>
|
|
||||||
<YAxis tickFormatter={(value) => `${value}%`} />
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Bar dataKey="errorRate" fill="hsl(var(--chart-1))" radius={8}>
|
|
||||||
<LabelList
|
|
||||||
position="top"
|
|
||||||
offset={12}
|
|
||||||
className="fill-foreground"
|
|
||||||
fontSize={12}
|
|
||||||
formatter={(value: number) => `${value}%`}
|
|
||||||
/>
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
|
||||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
|
||||||
|
|
||||||
export const description = "Ein Bereichsdiagramm zur Darstellung der prognostizierten Nutzung pro Wochentag";
|
|
||||||
|
|
||||||
interface ForecastData {
|
|
||||||
day: number; // 0 for Sunday, 1 for Monday, ..., 6 for Saturday
|
|
||||||
usageMinutes: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ForecastChartProps {
|
|
||||||
forecastData: ForecastData[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
usage: {
|
|
||||||
label: "Prognostizierte Nutzung",
|
|
||||||
color: "hsl(var(--chart-1))",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
const daysOfWeek = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
|
||||||
|
|
||||||
export function ForecastPrinterUsageChart({ forecastData }: ForecastChartProps) {
|
|
||||||
// Transform and slice data to fit the chart structure
|
|
||||||
const chartData = forecastData.map((data) => ({
|
|
||||||
//slice(1, forecastData.length - 1).
|
|
||||||
day: daysOfWeek[data.day], // Map day number to weekday name
|
|
||||||
usage: data.usageMinutes,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Prognostizierte Nutzung pro Wochentag</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer className="h-64 w-full" config={chartConfig}>
|
|
||||||
<AreaChart accessibilityLayer data={chartData} margin={{ left: 12, right: 12, top: 12 }}>
|
|
||||||
<CartesianGrid vertical={true} />
|
|
||||||
<XAxis dataKey="day" type="category" tickLine={true} tickMargin={10} axisLine={false} />
|
|
||||||
<YAxis type="number" dataKey="usage" tickLine={false} tickMargin={10} axisLine={false} />
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Area
|
|
||||||
dataKey="usage"
|
|
||||||
type="step"
|
|
||||||
fill="hsl(var(--chart-1))"
|
|
||||||
fillOpacity={0.4}
|
|
||||||
stroke="hsl(var(--chart-1))"
|
|
||||||
/>
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2 font-medium leading-none">
|
|
||||||
Zeigt die prognostizierte Nutzungszeit pro Wochentag in Minuten.
|
|
||||||
</div>
|
|
||||||
<div className="leading-none text-muted-foreground">
|
|
||||||
Besten Tage zur Wartung: {bestMaintenanceDays(forecastData)}
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bestMaintenanceDays(forecastData: ForecastData[]) {
|
|
||||||
const sortedData = forecastData.map((a) => a).sort((a, b) => a.usageMinutes - b.usageMinutes); // Sort ascending
|
|
||||||
|
|
||||||
const q1Index = Math.floor(sortedData.length * 0.33);
|
|
||||||
const q1 = sortedData[q1Index].usageMinutes; // First quartile (Q1) value
|
|
||||||
|
|
||||||
const filteredData = sortedData.filter((data) => data.usageMinutes <= q1);
|
|
||||||
|
|
||||||
return filteredData
|
|
||||||
.map((data) => {
|
|
||||||
const days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
|
||||||
return days[data.day];
|
|
||||||
})
|
|
||||||
.join(", ");
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { TrendingUp } from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
import { Label, Pie, PieChart } from "recharts";
|
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
|
||||||
|
|
||||||
export const description = "Nutzung des Druckers";
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
data: {
|
|
||||||
printerId: string;
|
|
||||||
utilizationPercentage: number;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {} satisfies ChartConfig;
|
|
||||||
|
|
||||||
export function PrinterUtilizationChart({ data }: ComponentProps) {
|
|
||||||
const totalUtilization = React.useMemo(() => data.utilizationPercentage, [data]);
|
|
||||||
const dataWithColor = {
|
|
||||||
...data,
|
|
||||||
fill: "rgb(34 197 94)",
|
|
||||||
};
|
|
||||||
const free = {
|
|
||||||
printerId: "-",
|
|
||||||
utilizationPercentage: 1 - data.utilizationPercentage,
|
|
||||||
name: "(Frei)",
|
|
||||||
fill: "rgb(212 212 212)",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="flex flex-col">
|
|
||||||
<CardHeader className="items-center pb-0">
|
|
||||||
<CardTitle>{data.name}</CardTitle>
|
|
||||||
<CardDescription>Nutzung des ausgewählten Druckers</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex-1 pb-0">
|
|
||||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
|
||||||
<PieChart>
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Pie
|
|
||||||
data={[dataWithColor, free]}
|
|
||||||
dataKey="utilizationPercentage"
|
|
||||||
nameKey="name"
|
|
||||||
innerRadius={60}
|
|
||||||
strokeWidth={5}
|
|
||||||
>
|
|
||||||
<Label
|
|
||||||
content={({ viewBox }) => {
|
|
||||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
|
||||||
return (
|
|
||||||
<text x={viewBox.cx} y={viewBox.cy} textAnchor="middle" dominantBaseline="middle">
|
|
||||||
<tspan x={viewBox.cx} y={viewBox.cy} className="fill-foreground text-3xl font-bold">
|
|
||||||
{(totalUtilization * 100).toFixed(2)}%
|
|
||||||
</tspan>
|
|
||||||
<tspan x={viewBox.cx} y={(viewBox.cy || 0) + 24} className="fill-muted-foreground">
|
|
||||||
Gesamt-Nutzung
|
|
||||||
</tspan>
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col gap-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2 font-medium leading-none">
|
|
||||||
Übersicht der Nutzung <TrendingUp className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
<div className="leading-none text-muted-foreground">Aktuelle Auslastung des Druckers</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from "recharts";
|
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
|
||||||
|
|
||||||
export const description = "Ein Balkendiagramm mit Beschriftung";
|
|
||||||
|
|
||||||
interface PrintVolumes {
|
|
||||||
today: number;
|
|
||||||
thisWeek: number;
|
|
||||||
thisMonth: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
volume: {
|
|
||||||
label: "Volumen",
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
interface PrinterVolumeChartProps {
|
|
||||||
printerVolume: PrintVolumes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PrinterVolumeChart({ printerVolume }: PrinterVolumeChartProps) {
|
|
||||||
const chartData = [
|
|
||||||
{ period: "Heute", volume: printerVolume.today, color: "hsl(var(--chart-1))" },
|
|
||||||
{ period: "Diese Woche", volume: printerVolume.thisWeek, color: "hsl(var(--chart-2))" },
|
|
||||||
{ period: "Diesen Monat", volume: printerVolume.thisMonth, color: "hsl(var(--chart-3))" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Druckvolumen</CardTitle>
|
|
||||||
<CardDescription>Vergleich: Heute, Diese Woche, Diesen Monat</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer className="h-64 w-full" config={chartConfig}>
|
|
||||||
<BarChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={chartData}
|
|
||||||
margin={{
|
|
||||||
top: 20,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} />
|
|
||||||
<XAxis
|
|
||||||
dataKey="period"
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => value}
|
|
||||||
/>
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Bar dataKey="volume" fill="var(--color-volume)" radius={8}>
|
|
||||||
<LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
|
|
||||||
</Bar>
|
|
||||||
</BarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
|
||||||
<div className="leading-none text-muted-foreground">
|
|
||||||
Zeigt das Druckvolumen für heute, diese Woche und diesen Monat
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
@ -1,135 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,204 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
return new Response(fs.readFileSync("./db/sqlite.db"));
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
"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}`;
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import { API_ENDPOINTS } from "@/utils/api-config";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: Request,
|
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const id = params.id;
|
|
||||||
|
|
||||||
// Rufe einzelnen Job vom externen Backend ab
|
|
||||||
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 502,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const job = await response.json();
|
|
||||||
return Response.json(job);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Abrufen des Jobs vom Backend:', error);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: Request,
|
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const id = params.id;
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Sende Job-Aktualisierung an das externe Backend
|
|
||||||
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 502,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
return Response.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Aktualisieren des Jobs:', error);
|
|
||||||
return new Response(JSON.stringify({ error: 'Fehler beim Aktualisieren des Jobs' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: Request,
|
|
||||||
{ params }: { params: { id: string } }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const id = params.id;
|
|
||||||
|
|
||||||
// Sende Job-Löschung an das externe Backend
|
|
||||||
const response = await fetch(`${API_ENDPOINTS.JOBS}/${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 502,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
return Response.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Löschen des Jobs:', error);
|
|
||||||
return new Response(JSON.stringify({ error: 'Fehler beim Löschen des Jobs' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
import { API_ENDPOINTS } from "@/utils/api-config";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
// Rufe Jobs vom externen Backend ab
|
|
||||||
const response = await fetch(API_ENDPOINTS.JOBS);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 502,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobs = await response.json();
|
|
||||||
return Response.json(jobs);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Abrufen der Jobs vom Backend:', error);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// Sende Job-Erstellung an das externe Backend
|
|
||||||
const response = await fetch(API_ENDPOINTS.JOBS, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 502,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
return Response.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Erstellen des Jobs:', error);
|
|
||||||
return new Response(JSON.stringify({ error: 'Fehler beim Erstellen des Jobs' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { API_ENDPOINTS } from "@/utils/api-config";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
// Rufe Drucker vom externen Backend ab statt von der lokalen Datenbank
|
|
||||||
const response = await fetch(API_ENDPOINTS.PRINTERS);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Backend-Fehler: ${response.status} ${response.statusText}`);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 502,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const printers = await response.json();
|
|
||||||
return Response.json(printers);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Fehler beim Abrufen der Drucker vom Backend:', error);
|
|
||||||
return new Response(JSON.stringify({ error: 'Backend nicht erreichbar' }), {
|
|
||||||
status: 500,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
import { lucia } from "@/server/auth";
|
|
||||||
import { type GitHubUserResult, github, isValidCallbackHost, USED_CALLBACK_URL } from "@/server/auth/oauth";
|
|
||||||
import { ALLOWED_CALLBACK_HOSTS } from "@/utils/api-config";
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Log für Debugging
|
|
||||||
console.log("OAuth Callback erhalten:", url.toString());
|
|
||||||
console.log("Callback URL Validierung:", isValidCallbackHost(url.toString()));
|
|
||||||
console.log("Erlaubte Hosts:", ALLOWED_CALLBACK_HOSTS);
|
|
||||||
|
|
||||||
if (!code || !state || !storedState || state !== storedState) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
status_text: "Ungültiger OAuth-Callback",
|
|
||||||
data: { code, state, storedState, url: url.toString() },
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 400,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// GitHub OAuth Code validieren - die redirectURI ist bereits im GitHub Client konfiguriert
|
|
||||||
const tokens = await github.validateAuthorizationCode(code);
|
|
||||||
|
|
||||||
// Log zur Fehlersuche
|
|
||||||
console.log(`GitHub OAuth Token-Validierung erfolgreich, verwendete Callback-URL: ${USED_CALLBACK_URL}`);
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { github, USED_CALLBACK_URL } 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();
|
|
||||||
|
|
||||||
// Verwende die zentral definierte Callback-URL
|
|
||||||
// Die redirectURI ist bereits im GitHub-Client konfiguriert
|
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log zur Fehlersuche
|
|
||||||
console.log(`GitHub OAuth redirect zu: ${url.toString()}`);
|
|
||||||
console.log(`Verwendete Callback-URL: ${USED_CALLBACK_URL}`);
|
|
||||||
|
|
||||||
return Response.redirect(url);
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 166 KiB |
@ -1,61 +0,0 @@
|
|||||||
@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%;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
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"),
|
|
||||||
*/
|
|
@ -1,36 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,141 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
@ -1,73 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
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} — {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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default function NotFound() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Nicht gefunden</h2>
|
|
||||||
<p>Die angefragte Seite konnte nicht gefunden werden.</p>
|
|
||||||
<Link href="/">Zurück zur Startseite</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
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"> </p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
"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} />;
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
|
||||||
import { ScanFaceIcon } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function LoginButton() {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [isLocked, setLocked] = useState(false);
|
|
||||||
function onClick() {
|
|
||||||
if (!isLocked) {
|
|
||||||
toast({
|
|
||||||
description: "Du wirst angemeldet...",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prevent multiple clicks because of login delay...
|
|
||||||
setLocked(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setLocked(false);
|
|
||||||
}, 1000 * 5);
|
|
||||||
}
|
|
||||||
toast({
|
|
||||||
description: "Bitte warte einen Moment...",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={onClick} variant={"ghost"} className="gap-2 flex items-center" asChild disabled={isLocked}>
|
|
||||||
<Link href="/auth/login">
|
|
||||||
<ScanFaceIcon className="w-4 h-4" />
|
|
||||||
<span>Anmelden</span>
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user