torben fronted wiederhergestellt
This commit is contained in:
27
packages/reservation-platform/.dockerignore
Normal file
27
packages/reservation-platform/.dockerignore
Normal file
@ -0,0 +1,27 @@
|
||||
# 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/
|
@ -1,10 +0,0 @@
|
||||
# Basic Server Configuration
|
||||
RUNTIME_ENVIRONMENT=prod
|
||||
DB_PATH=db/sqlite.db
|
||||
|
||||
# OAuth Configuration (Bitte anpassen)
|
||||
OAUTH_CLIENT_ID=client_id
|
||||
OAUTH_CLIENT_SECRET=client_secret
|
||||
|
||||
# Backend-API URL (IP-Adresse oder Hostname des Backend-Servers)
|
||||
NEXT_PUBLIC_API_URL=http://localhost:5000
|
@ -1,7 +1,3 @@
|
||||
# Basic Server Configuration
|
||||
RUNTIME_ENVIRONMENT=dev
|
||||
DB_PATH=db/sqlite.db
|
||||
|
||||
# OAuth Configuration
|
||||
OAUTH_CLIENT_ID=client_id
|
||||
OAUTH_CLIENT_SECRET=client_secret
|
5
packages/reservation-platform/.gitignore
vendored
5
packages/reservation-platform/.gitignore
vendored
@ -1,7 +1,10 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# db folder
|
||||
/db
|
||||
db/
|
||||
|
||||
# Env file
|
||||
.env
|
||||
|
||||
|
||||
# dependencies
|
||||
|
@ -1,88 +1,34 @@
|
||||
FROM node:alpine
|
||||
FROM node:20-bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
# Create application directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
# Install system dependencies for SQLite and native modules
|
||||
RUN apk add --no-cache python3 build-base g++ make sqlite sqlite-dev gcc musl-dev git libffi-dev openssl-dev cmake
|
||||
# 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 npm install -g pnpm
|
||||
RUN corepack enable pnpm
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
# Install dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# Install dependencies with native bindings build approval, ensuring to build from source for all platforms
|
||||
ENV CFLAGS="-fPIC" \
|
||||
LDFLAGS="-fPIC" \
|
||||
CXXFLAGS="-fPIC" \
|
||||
npm_config_build_from_source=true \
|
||||
npm_config_sqlite=/usr/local \
|
||||
npm_config_sqlite_libname=sqlite3
|
||||
# Copy the rest of the application code
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Durchführen der Installation mit umfassenden Flags für native Bindungen
|
||||
RUN pnpm install --unsafe-perm --no-optional --frozen-lockfile
|
||||
# Initialize Database, if it not already exists
|
||||
RUN pnpm run db
|
||||
|
||||
# Hinweis: better-sqlite3 neu bauen verursacht Fehler mit Node 23.10
|
||||
# npx ist bereits in Node.js integriert - kein zusätzlicher Install nötig
|
||||
# Build the application
|
||||
RUN pnpm run build
|
||||
|
||||
# Install tsx for running TypeScript files directly
|
||||
RUN pnpm add -D tsx
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Create database directory
|
||||
RUN mkdir -p db/
|
||||
|
||||
# Build the Next.js application
|
||||
RUN pnpm build || echo "Generate schema failed, but continuing..."
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 3000
|
||||
|
||||
# Startup script with robust JSON fallback approach
|
||||
RUN echo '#!/bin/sh' > /app/startup.sh && \
|
||||
echo 'set -e' >> /app/startup.sh && \
|
||||
echo 'mkdir -p /app/db' >> /app/startup.sh && \
|
||||
echo 'echo "Starting application..."' >> /app/startup.sh && \
|
||||
echo 'echo "Konfiguriere DB-Verzeichnis..."' >> /app/startup.sh && \
|
||||
echo 'DB_FILE="/app/db/sqlite.db"' >> /app/startup.sh && \
|
||||
echo 'DB_JSON="/app/db/db.json"' >> /app/startup.sh && \
|
||||
echo 'if [ ! -f "$DB_FILE" ]; then' >> /app/startup.sh && \
|
||||
echo ' echo "Creating empty database file..."' >> /app/startup.sh && \
|
||||
echo ' touch "$DB_FILE"' >> /app/startup.sh && \
|
||||
echo 'fi' >> /app/startup.sh && \
|
||||
echo 'if [ ! -f "$DB_JSON" ]; then' >> /app/startup.sh && \
|
||||
echo ' echo "Creating empty JSON DB file (fallback)..."' >> /app/startup.sh && \
|
||||
echo ' echo "{}" > "$DB_JSON"' >> /app/startup.sh && \
|
||||
echo 'fi' >> /app/startup.sh && \
|
||||
echo 'chmod 666 "$DB_FILE"' >> /app/startup.sh && \
|
||||
echo 'chmod 666 "$DB_JSON"' >> /app/startup.sh && \
|
||||
echo 'chmod -R 777 /app/db' >> /app/startup.sh && \
|
||||
echo 'export DB_PATH=$DB_FILE' >> /app/startup.sh && \
|
||||
echo 'export DB_JSON_PATH=$DB_JSON' >> /app/startup.sh && \
|
||||
echo 'echo "Datenbank wird unter $DB_PATH verwendet"' >> /app/startup.sh && \
|
||||
echo 'echo "JSON Fallback unter $DB_JSON_PATH"' >> /app/startup.sh && \
|
||||
echo '' >> /app/startup.sh && \
|
||||
echo '# Try to rebuild better-sqlite3 for current platform, but continue if it fails' >> /app/startup.sh && \
|
||||
echo 'if [ ! -d "/app/node_modules/.pnpm/better-sqlite3@9.6.0/node_modules/better-sqlite3/build" ]; then' >> /app/startup.sh && \
|
||||
echo ' echo "SQLite Bindings nicht gefunden, versuche sie zu bauen..."' >> /app/startup.sh && \
|
||||
echo ' cd /app && CFLAGS="-fPIC" LDFLAGS="-fPIC" CXXFLAGS="-fPIC" npm_config_build_from_source=true npm_config_sqlite=/usr/local npm_config_sqlite_libname=sqlite3 pnpm rebuild better-sqlite3 || echo "SQLite Rebuild fehlgeschlagen - wird JSON-Fallback verwenden"' >> /app/startup.sh && \
|
||||
echo 'fi' >> /app/startup.sh && \
|
||||
echo '' >> /app/startup.sh && \
|
||||
echo 'echo "Führe Datenbank-Migration aus..."' >> /app/startup.sh && \
|
||||
echo 'NODE_ENV=production npx tsx ./src/server/db/migrate.ts || echo "SQLite Migration fehlgeschlagen - wird beim Neustart erneut versucht"' >> /app/startup.sh && \
|
||||
echo 'echo "Migration abgeschlossen"' >> /app/startup.sh && \
|
||||
echo '' >> /app/startup.sh && \
|
||||
echo 'echo "Starte Next.js Anwendung..."' >> /app/startup.sh && \
|
||||
echo 'if [ -d ".next" ]; then' >> /app/startup.sh && \
|
||||
echo ' NODE_OPTIONS="--no-warnings" pnpm start' >> /app/startup.sh && \
|
||||
echo 'else' >> /app/startup.sh && \
|
||||
echo ' echo "Build directory nicht gefunden, führe Build aus..."' >> /app/startup.sh && \
|
||||
echo ' NODE_OPTIONS="--no-warnings" pnpm build || echo "Build fehlgeschlagen - wird beim Neustart erneut versucht"' >> /app/startup.sh && \
|
||||
echo ' NODE_OPTIONS="--no-warnings" pnpm start || NODE_OPTIONS="--no-warnings" pnpm dev' >> /app/startup.sh && \
|
||||
echo 'fi' >> /app/startup.sh && \
|
||||
chmod +x /app/startup.sh
|
||||
|
||||
# Start the application
|
||||
CMD ["/app/startup.sh"]
|
||||
CMD ["/bin/sh", "-c", "if [ ! -f ./db/sqlite.db ]; then pnpm db; fi && pnpm start"]
|
||||
|
@ -1,217 +1,32 @@
|
||||
utilss/analytics/(scope).ts
|
||||
deriver.ts
|
||||
utils/sentinel.ts -> auth guard
|
||||
# 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
|
||||
|
||||
Basierend auf den erwähnten Anforderungen, hier sind einige zusätzliche Spalten, die Sie zu Ihrer Datenbank hinzufügen könnten:
|
||||
### Voraussetzungen
|
||||
|
||||
Für die Tabelle printers:
|
||||
- Netzwerk auf Raspberry Pi ist eingerichtet
|
||||
- Docker ist installiert
|
||||
|
||||
total_print_jobs: Die Gesamtzahl der Druckaufträge, die ein Drucker ausgeführt hat.
|
||||
### Schritte
|
||||
|
||||
total_active_time: Die Gesamtzeit, in der der Drucker aktiv war (in Minuten).
|
||||
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)
|
||||
|
||||
total_error_jobs: Die Gesamtzahl der Druckaufträge, die aufgrund eines Fehlers abgebrochen wurden.
|
||||
## Entwicklerinformationen
|
||||
|
||||
last_maintenance_date: Das Datum der letzten Wartung des Druckers.
|
||||
### Raspberry Pi Einstellungen
|
||||
|
||||
Für die Tabelle printJobs:
|
||||
Auf dem Raspberry Pi wurde Raspbian Lite installiert.
|
||||
Unter /srv/* sind die Projektdateien zu finden.
|
||||
|
||||
end_time: Die Zeit, zu der der Druckauftrag beendet wurde.
|
||||
### Anmeldedaten
|
||||
|
||||
was_successful: Ein boolescher Wert, der angibt, ob der Druckauftrag erfolgreich abgeschlossen wurde.
|
||||
|
||||
error_code: Ein Code, der einen bestimmten Fehler identifiziert, wenn der Druckauftrag abgebrochen wurde.
|
||||
|
||||
Für die Tabelle users:
|
||||
|
||||
total_print_jobs: Die Gesamtzahl der Druckaufträge, die ein Benutzer gestartet hat.
|
||||
|
||||
preferred_printer_id: Die ID des Druckers, den der Benutzer am häufigsten verwendet.
|
||||
|
||||
last_login_date: Das Datum des letzten Logins des Benutzers.
|
||||
|
||||
Diese zusätzlichen Spalten könnten Ihnen dabei helfen, die oben genannten statistischen Analysen und Machine Learning-Vorhersagen durchzuführen. Bitte beachten Sie, dass Sie möglicherweise zusätzliche Logik in Ihrer Anwendung implementieren müssen, um diese Spalten zu aktualisieren, wenn entsprechende Ereignisse eintreten (z.B. ein Druckauftrag wird gestartet oder beendet, ein Benutzer loggt sich ein usw.).
|
||||
|
||||
---
|
||||
|
||||
Basierend auf Ihrem Datenbankschema, das Informationen über Drucker, Druckaufträge und Benutzer enthält, könnten Sie eine Vielzahl von statistischen Analysen und Machine Learning-Vorhersagen treffen. Hier sind einige konkrete Vorschläge:
|
||||
|
||||
### Statistische Analysen:
|
||||
1. **Auslastungsanalyse**: Bestimmen Sie die Auslastung der Drucker, indem Sie die Anzahl und Dauer der Druckaufträge analysieren.
|
||||
2. **Fehleranalyse**: Untersuchen Sie die Häufigkeit und Ursachen von abgebrochenen Druckaufträgen, um Muster zu erkennen.
|
||||
3. **Benutzerverhalten**: Analysieren Sie das Verhalten der Benutzer, z.B. welche Drucker am häufigsten verwendet werden oder zu welchen Zeiten die meisten Druckaufträge eingehen.
|
||||
|
||||
### Machine Learning-Vorhersagen:
|
||||
1. **Vorhersage der Druckerauslastung**: Verwenden Sie Zeitreihenanalysen, um zukünftige Auslastungsmuster der Drucker vorherzusagen.
|
||||
2. **Anomalieerkennung**: Setzen Sie Machine Learning ein, um Anomalien im Druckverhalten zu erkennen, die auf potenzielle Probleme hinweisen könnten.
|
||||
3. **Empfehlungssystem**: Entwickeln Sie ein Modell, das Benutzern basierend auf ihren bisherigen Druckaufträgen und Präferenzen Drucker empfiehlt.
|
||||
|
||||
### Konkrete Umsetzungsempfehlungen:
|
||||
- **Daten vorbereiten**: Reinigen und transformieren Sie Ihre Daten, um sie für die Analyse vorzubereiten. Entfernen Sie Duplikate, behandeln Sie fehlende Werte und konvertieren Sie kategoriale Daten in ein format, das von Machine Learning-Algorithmen verarbeitet werden kann.
|
||||
- **Feature Engineering**: Erstellen Sie neue Merkmale (Features), die für Vorhersagemodelle nützlich sein könnten, wie z.B. die durchschnittliche Dauer der Druckaufträge pro Benutzer oder die Gesamtzahl der Druckaufträge pro Drucker.
|
||||
- **Modellauswahl**: Wählen Sie geeignete Machine Learning-Modelle aus. Für Zeitreihenprognosen könnten ARIMA-Modelle geeignet sein, während für die Klassifizierung von Benutzerverhalten Entscheidungsbäume oder Random Forests verwendet werden könnten.
|
||||
- **Modelltraining und -validierung**: Trainieren Sie Ihre Modelle mit einem Teil Ihrer Daten und validieren Sie sie mit einem anderen Teil, um sicherzustellen, dass die Modelle gut generalisieren und nicht überangepasst sind.
|
||||
- **Ergebnisinterpretation**: Interpretieren Sie die Ergebnisse Ihrer Modelle und nutzen Sie sie, um geschäftliche Entscheidungen zu treffen oder die Benutzererfahrung auf Ihrer Plattform zu verbessern.
|
||||
|
||||
Diese Vorschläge sind abhängig von der Qualität und Quantität Ihrer Daten sowie den spezifischen Zielen, die Sie mit Ihrer Plattform verfolgen. Es ist wichtig, dass Sie die Modelle regelmäßig aktualisieren, um die Genauigkeit der Vorhersagen zu erhalten und zu verbessern.
|
||||
|
||||
Quelle: Unterhaltung mit Bing, 11.5.2024
|
||||
(1) Data Science – Nutzung von KI für Predictive Analytics - Springer. https://link.springer.com/content/pdf/10.1007/978-3-658-33731-5_27.pdf.
|
||||
(2) Predictive Analytics: Grundlagen, Projektbeispiele und Lessons ... - Haufe. https://www.haufe.de/finance/haufe-finance-office-premium/predictive-analytics-grundlagen-projektbeispiele-und-lessons-learned_idesk_PI20354_HI13561373.html.
|
||||
(3) Predictive Modelling: Was es ist und wie es dir dabei helfen kann, dein .... https://www.acquisa.de/magazin/predictive-modelling.
|
||||
(4) Deep Learning und Predictive Analytics: Vorhersage von Kundenverhalten .... https://www.hagel-it.de/it-insights/deep-learning-und-predictive-analytics-vorhersage-von-kundenverhalten-und-markttrends.html.
|
||||
(5) undefined. https://doi.org/10.1007/978-3-658-33731-5_27.
|
||||
|
||||
---
|
||||
|
||||
https://github.com/drizzle-team/drizzle-orm/discussions/1480#discussioncomment-9363695
|
||||
|
||||
---
|
||||
|
||||
Um eine 3D-Drucker Reservierungsplattform zu entwickeln und die genannten Kriterien umzusetzen, empfehle ich folgende Schritte:
|
||||
|
||||
### Kundenspezifische Anforderungen analysieren:
|
||||
1. **Stakeholder-Interviews** durchführen, um Bedürfnisse und Erwartungen zu verstehen.
|
||||
2. **Umfragen** erstellen, um Feedback von potenziellen Nutzern zu sammeln.
|
||||
3. **Anforderungsworkshops** abhalten, um gemeinsam mit den Stakeholdern Anforderungen zu definieren.
|
||||
4. **User Stories** und **Use Cases** entwickeln, um die Anforderungen zu konkretisieren.
|
||||
|
||||
### Projektumsetzung planen:
|
||||
1. **Projektziele** klar definieren und mit den betrieblichen Zielen abstimmen.
|
||||
2. **Ressourcenplanung** vornehmen, um Personal, Zeit und Budget effizient einzusetzen.
|
||||
3. **Risikoanalyse** durchführen, um potenzielle Hindernisse frühzeitig zu erkennen.
|
||||
4. **Meilensteinplanung** erstellen, um wichtige Projektphasen zu strukturieren.
|
||||
|
||||
### Daten identifizieren, klassifizieren und modellieren:
|
||||
1. **Datenquellen** identifizieren, die für die Reservierungsplattform relevant sind.
|
||||
2. **Datenklassifikation** vornehmen, um die Daten nach Typ und Sensibilität zu ordnen.
|
||||
3. **Entity-Relationship-Modelle** (ERM) erstellen, um die Beziehungen zwischen den Daten zu visualisieren.
|
||||
|
||||
### Mathematische Vorhersagemodelle und statistische Verfahren nutzen:
|
||||
1. **Regressionsanalysen** durchführen, um zukünftige Nutzungsmuster vorherzusagen.
|
||||
2. **Clusteranalysen** anwenden, um Nutzergruppen zu identifizieren und zu segmentieren.
|
||||
3. **Zeitreihenanalysen** nutzen, um Trends und saisonale Schwankungen zu erkennen.
|
||||
|
||||
### Datenqualität sicherstellen:
|
||||
1. **Validierungsregeln** implementieren, um die Eingabe korrekter Daten zu gewährleisten.
|
||||
2. **Datenbereinigung** regelmäßig durchführen, um Duplikate und Inkonsistenzen zu entfernen.
|
||||
3. **Datenintegrität** durch Referenzintegritätsprüfungen sicherstellen.
|
||||
|
||||
### Analyseergebnisse aufbereiten und Optimierungsmöglichkeiten aufzeigen:
|
||||
1. **Dashboards** entwickeln, um die wichtigsten Kennzahlen übersichtlich darzustellen.
|
||||
2. **Berichte** generieren, die detaillierte Einblicke in die Nutzungsdaten bieten.
|
||||
3. **Handlungsempfehlungen** ableiten, um die Plattform kontinuierlich zu verbessern.
|
||||
|
||||
### Projektdokumentation anforderungsgerecht erstellen:
|
||||
1. **Dokumentationsstandards** festlegen, um Einheitlichkeit zu gewährleisten.
|
||||
2. **Versionskontrolle** nutzen, um Änderungen nachvollziehbar zu machen.
|
||||
3. **Projektfortschritt** dokumentieren, um den Überblick über den aktuellen Stand zu behalten.
|
||||
|
||||
Diese Empfehlungen sollen als Leitfaden dienen, um die genannten Kriterien systematisch und strukturiert in Ihrem Abschlussprojekt umzusetzen.
|
||||
|
||||
Quelle: Unterhaltung mit Bing, 11.5.2024
|
||||
(1) Erfolgreiche Datenanalyseprojekte: Diese Strategien sollten Sie kennen. https://www.b2bsmartdata.de/blog/erfolgreiche-datenanalyseprojekte-diese-strategien-sollten-sie-kennen.
|
||||
(2) Projektdokumentation - wichtige Grundregeln | dieprojektmanager. https://dieprojektmanager.com/projektdokumentation-wichtige-grundregeln/.
|
||||
(3) Projektdokumentation: Definition, Aufbau, Inhalte und Beispiel. https://www.wirtschaftswissen.de/unternehmensfuehrung/projektmanagement/projektdokumentation-je-genauer-sie-ist-desto-weniger-arbeit-haben-sie-mit-nachfolgeprojekten/.
|
||||
(4) Was ist Datenmodellierung? | IBM. https://www.ibm.com/de-de/topics/data-modeling.
|
||||
(5) Was ist Datenmodellierung? | Microsoft Power BI. https://powerbi.microsoft.com/de-de/what-is-data-modeling/.
|
||||
(6) Inhalte Datenmodelle und Datenmodellierung Datenmodellierung ... - TUM. https://wwwbroy.in.tum.de/lehre/vorlesungen/mbe/SS07/vorlfolien/02_Datenmodellierung.pdf.
|
||||
(7) Definition von Datenmodellierung: Einsatzbereiche und Typen.. https://business.adobe.com/de/blog/basics/define-data-modeling.
|
||||
(8) 3. Informations- und Datenmodelle - RPTU. http://lgis.informatik.uni-kl.de/archiv/wwwdvs.informatik.uni-kl.de/courses/DBS/WS2000/Vorlesungsunterlagen/Kapitel.03.pdf.
|
||||
(9) Prozessoptimierung: 7 Methoden im Überblick! [2024] • Asana. https://asana.com/de/resources/process-improvement-methodologies.
|
||||
(10) Prozessoptimierung: Definition, Methoden & Praxis-Beispiele. https://peras.de/hr-blog/detail/hr-blog/prozessoptimierung.
|
||||
(11) Optimierungspotenzial erkennen - OPTANO. https://optano.com/blog/optimierungspotenzial-erkennen/.
|
||||
(12) Projektplanung: Definition, Ziele und Ablauf - wirtschaftswissen.de. https://www.wirtschaftswissen.de/unternehmensfuehrung/projektmanagement/in-nur-5-schritten-zur-fehlerfreien-projektplanung/.
|
||||
(13) Projektphasen: Die Vier! Von der Planung zur Umsetzung. https://www.pureconsultant.de/de/wissen/projektphasen/.
|
||||
(14) Hinweise zur Abschlussprüfung in den IT-Berufen (VO 2020) - IHK_DE. https://www.ihk.de/blueprint/servlet/resource/blob/5361152/008d092b38f621b2c97c66d5193d9f6c/pruefungshinweise-neue-vo-2020-data.pdf.
|
||||
(15) PAO – Projektantrag Fachinformatiker Daten- und Prozessanalyse - IHK_DE. https://www.ihk.de/blueprint/servlet/resource/blob/5673390/37eb05e451ed6051f6316f66d012cc50/projektantrag-fachinformatiker-daten-und-prozessanalyse-data.pdf.
|
||||
(16) IT-BERUFE Leitfaden zur IHK-Abschlussprüfung Fachinformatikerinnen und .... https://www.ihk.de/blueprint/servlet/resource/blob/5439816/6570224fb196bc7e10d16beeeb75fec1/neu-leitfaden-fian-data.pdf.
|
||||
(17) Fachinformatiker/-in Daten- und Prozessanalyse - IHK Nord Westfalen. https://www.ihk.de/nordwestfalen/bildung/ausbildung/ausbildungsberufe-a-z/fachinformatiker-daten-und-prozessanalyse-4767680.
|
||||
(18) Leitfaden zur IHK-Abschlussprüfung Fachinformatiker/-in .... https://www.ihk.de/blueprint/servlet/resource/blob/5682602/2fbedf4b4f33f7522d28ebc611adc909/fachinformatikerin-daten-und-prozessanalyse-data.pdf.
|
||||
(19) § 28 FIAusbV - Einzelnorm - Gesetze im Internet. https://www.gesetze-im-internet.de/fiausbv/__28.html.
|
||||
(20) Hinweise des Prüfungsausschusses zur Projektarbeit. https://www.neubrandenburg.ihk.de/fileadmin/user_upload/Aus_und_Weiterbildung/Ausbildung/Projektarbeit_Fachinformatiker_FR._Daten-_und_Prozessanalyse.pdf.
|
||||
(21) Datenqualität: Definition und Methoden zur kontinuierlichen .... https://www.acquisa.de/magazin/datenqualitaet.
|
||||
(22) Datenqualität: Definition, Merkmale und Analyse (Guide) - Kobold AI. https://www.kobold.ai/datenqualitaet-guide/.
|
||||
(23) Datenqualität: Definition und Methoden zur kontinuierlichen .... https://bing.com/search?q=Sicherstellung+der+Datenqualit%c3%a4t.
|
||||
(24) Datenqualitätsmanagement: Sicherstellung hoher Datenstandards. https://www.data-analyst.de/glossar/data-quality-management/.
|
||||
(25) Kundenspezifische Anforderungen CSR - Beratung für Managementsysteme. https://smct-management.de/kundenspezifische-anforderungen-csr-im-sinne-der-iatf-16949/.
|
||||
(26) CSR Sys - Kundenspezifische Anforderungen verwalten und bewerten. https://smct-management.de/csr-sys-kundenspezifische-anforderungen/.
|
||||
(27) Beauftragter für Customer Specific Requirements (CSR). https://www.tuev-nord.de/de/weiterbildung/seminare/beauftragter-fuer-customer-specific-requirements-csr-a/.
|
||||
(28) Kundenspezifische Anforderungen Seminar | Jetzt anfragen! - qdc. https://qdc.de/kundenspezifische-anforderungen-seminar/.
|
||||
|
||||
---
|
||||
|
||||
Um die Punkte zur Datenidentifikation, -klassifikation, -modellierung und zur Nutzung mathematischer Modelle und statistischer Verfahren weiter zu konkretisieren, finden Sie hier detaillierte Empfehlungen:
|
||||
|
||||
### Datenquellen identifizieren:
|
||||
1. **Bestandsaufnahme** der aktuellen Daten: Erfassen Sie alle Daten, die bereits im Unternehmen vorhanden sind, wie z.B. Kundeninformationen, Transaktionsdaten und Gerätenutzungsdaten.
|
||||
2. **Externe Datenquellen** prüfen: Untersuchen Sie, ob und welche externen Datenquellen wie Materiallieferanten oder Wartungsdienstleister relevant sein könnten.
|
||||
3. **IoT-Sensordaten**: Berücksichtigen Sie die Integration von IoT-Geräten, die in Echtzeit Daten über den Zustand und die Nutzung der 3D-Drucker liefern.
|
||||
|
||||
### Datenklassifikation:
|
||||
1. **Sensibilitätsstufen** festlegen: Bestimmen Sie, welche Daten sensibel sind (z.B. personenbezogene Daten) und einer besonderen Schutzstufe bedürfen.
|
||||
2. **Datenkategorien** erstellen: Ordnen Sie die Daten in Kategorien wie Nutzungsdaten, Finanzdaten, Betriebsdaten etc.
|
||||
3. **Zugriffsrechte** definieren: Legen Sie fest, wer Zugriff auf welche Daten haben darf, um die Datensicherheit zu gewährleisten.
|
||||
|
||||
### Entity-Relationship-Modelle (ERM):
|
||||
1. **Datenentitäten** identifizieren: Bestimmen Sie die Kernentitäten wie Benutzer, Drucker, Reservierungen und Materialien.
|
||||
2. **Beziehungen** festlegen: Definieren Sie, wie diese Entitäten miteinander in Beziehung stehen (z.B. ein Benutzer kann mehrere Reservierungen haben).
|
||||
3. **ERM-Tools** nutzen: Verwenden Sie Software wie Lucidchart oder Microsoft Visio, um die ERMs zu visualisieren.
|
||||
|
||||
### Regressionsanalysen:
|
||||
1. **Historische Daten** sammeln: Nutzen Sie vergangene Nutzungsdaten, um Muster zu erkennen.
|
||||
2. **Prädiktive Variablen** wählen: Identifizieren Sie Faktoren, die die Nutzung beeinflussen könnten, wie z.B. Uhrzeit, Wochentag oder Materialtyp.
|
||||
3. **Regressionsmodelle** anwenden: Nutzen Sie lineare oder logistische Regression, um zukünftige Nutzungsmuster vorherzusagen.
|
||||
|
||||
### Clusteranalysen:
|
||||
1. **Nutzersegmentierung**: Teilen Sie Nutzer basierend auf ihrem Verhalten in Gruppen ein, z.B. nach Häufigkeit der Nutzung oder bevorzugten Materialien.
|
||||
2. **K-Means-Clustering**: Verwenden Sie Algorithmen wie K-Means, um die Nutzer in sinnvolle Cluster zu segmentieren.
|
||||
3. **Cluster-Validierung**: Überprüfen Sie die Güte der Clusterbildung, um sicherzustellen, dass die Segmente aussagekräftig sind.
|
||||
|
||||
### Zeitreihenanalysen:
|
||||
1. **Zeitstempel-Daten** analysieren: Untersuchen Sie Daten mit Zeitstempeln, um Trends und Muster über die Zeit zu erkennen.
|
||||
2. **Saisonale Effekte** berücksichtigen: Identifizieren Sie saisonale Schwankungen in der Nutzung der 3D-Drucker.
|
||||
3. **ARIMA-Modelle**: Nutzen Sie autoregressive integrierte gleitende Durchschnitte (ARIMA), um zukünftige Trends zu prognostizieren.
|
||||
|
||||
Diese Methoden helfen Ihnen, ein tiefes Verständnis der Daten zu entwickeln, das für die erfolgreiche Umsetzung Ihrer Reservierungsplattform unerlässlich ist. Denken Sie daran, dass die genaue Anwendung dieser Techniken von den spezifischen Daten und Anforderungen Ihres Projekts abhängt. Es ist wichtig, dass Sie sich mit den Grundlagen der Datenanalyse und statistischen Modellierung vertraut machen, um diese Methoden effektiv anwenden zu können.
|
||||
|
||||
----
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
Benutzer: myp
|
||||
Passwort: (persönlich bekannt)
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
@ -1,23 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: myp-frontend
|
||||
network_mode: host
|
||||
environment:
|
||||
- RUNTIME_ENVIRONMENT=${RUNTIME_ENVIRONMENT:-prod}
|
||||
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID:-client_id}
|
||||
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET:-client_secret}
|
||||
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:5000}
|
||||
volumes:
|
||||
- ./db:/app/db
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
31
packages/reservation-platform/docker/build.sh
Normal file
31
packages/reservation-platform/docker/build.sh
Normal file
@ -0,0 +1,31 @@
|
||||
#!/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
|
8
packages/reservation-platform/docker/caddy/Caddyfile
Normal file
8
packages/reservation-platform/docker/caddy/Caddyfile
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
debug
|
||||
}
|
||||
|
||||
m040tbaraspi001.de040.corpintra.net, m040tbaraspi001.de040.corpinter.net {
|
||||
reverse_proxy myp-rp:3000
|
||||
tls internal
|
||||
}
|
19
packages/reservation-platform/docker/compose.yml
Normal file
19
packages/reservation-platform/docker/compose.yml
Normal file
@ -0,0 +1,19 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:2.8
|
||||
container_name: caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- ./caddy/data:/data
|
||||
- ./caddy/config:/config
|
||||
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
myp-rp:
|
||||
image: myp-rp:latest
|
||||
container_name: myp-rp
|
||||
env_file: "/srv/myp-env/github.env"
|
||||
volumes:
|
||||
- /srv/MYP-DB:/usr/src/app/db
|
||||
restart: unless-stopped
|
36
packages/reservation-platform/docker/deploy.sh
Normal file
36
packages/reservation-platform/docker/deploy.sh
Normal file
@ -0,0 +1,36 @@
|
||||
#!/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"
|
2
packages/reservation-platform/docker/images/.gitattributes
vendored
Normal file
2
packages/reservation-platform/docker/images/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
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)
Normal file
BIN
packages/reservation-platform/docker/images/caddy_2.8.tar.xz
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
packages/reservation-platform/docker/images/myp-rp_latest.tar.xz
(Stored with Git LFS)
Normal file
BIN
packages/reservation-platform/docker/images/myp-rp_latest.tar.xz
(Stored with Git LFS)
Normal file
Binary file not shown.
68
packages/reservation-platform/docker/save.sh
Normal file
68
packages/reservation-platform/docker/save.sh
Normal file
@ -0,0 +1,68 @@
|
||||
#!/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
|
116
packages/reservation-platform/docs/Admin-Dashboard.md
Normal file
116
packages/reservation-platform/docs/Admin-Dashboard.md
Normal file
@ -0,0 +1,116 @@
|
||||
# **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)
|
79
packages/reservation-platform/docs/Architektur.md
Normal file
79
packages/reservation-platform/docs/Architektur.md
Normal file
@ -0,0 +1,79 @@
|
||||
# **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)
|
150
packages/reservation-platform/docs/Bereitstellungsdetails .md
Normal file
150
packages/reservation-platform/docs/Bereitstellungsdetails .md
Normal file
@ -0,0 +1,150 @@
|
||||
# **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)
|
153
packages/reservation-platform/docs/Datenbank.md
Normal file
153
packages/reservation-platform/docs/Datenbank.md
Normal file
@ -0,0 +1,153 @@
|
||||
# **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)
|
93
packages/reservation-platform/docs/Installation.md
Normal file
93
packages/reservation-platform/docs/Installation.md
Normal file
@ -0,0 +1,93 @@
|
||||
# **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)
|
75
packages/reservation-platform/docs/Nutzung.md
Normal file
75
packages/reservation-platform/docs/Nutzung.md
Normal file
@ -0,0 +1,75 @@
|
||||
# **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)
|
37
packages/reservation-platform/docs/README.md
Normal file
37
packages/reservation-platform/docs/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# **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)
|
@ -5,8 +5,8 @@ export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
schema: "./src/server/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
driver: "better-sqlite",
|
||||
driver: "libsql",
|
||||
dbCredentials: {
|
||||
url: "db/sqlite.db",
|
||||
url: "file:./db/sqlite.db",
|
||||
},
|
||||
});
|
||||
|
@ -1,4 +1,26 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const 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,7 +1,8 @@
|
||||
{
|
||||
"name": "myp-rp",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.1",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
@ -15,59 +16,68 @@
|
||||
"db:reset": "pnpm db:clean && pnpm db"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.0.3",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@lucia-auth/adapter-drizzle": "^1.0.7",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@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.0.2",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@tanstack/react-table": "^8.16.0",
|
||||
"@tremor/react": "^3.16.2",
|
||||
"arctic": "^1.8.1",
|
||||
"better-sqlite3": "^9.6.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",
|
||||
"drizzle-json-db": "^0.1.1",
|
||||
"lucia": "^3.2.0",
|
||||
"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.0",
|
||||
"oslo": "^1.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"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.4.41",
|
||||
"sonner": "^1.5.0",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-debounce": "^10.0.0",
|
||||
"use-debounce": "^10.0.3",
|
||||
"uuid": "^11.0.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.7.3",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/node": "^20.12.11",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"drizzle-kit": "^0.21.1",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
2561
packages/reservation-platform/pnpm-lock.yaml
generated
2561
packages/reservation-platform/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +0,0 @@
|
||||
ignoredBuiltDependencies:
|
||||
- '@biomejs/biome'
|
||||
- better-sqlite3
|
||||
- es5-ext
|
||||
- esbuild
|
9279
packages/reservation-platform/repomix-output.txt
Normal file
9279
packages/reservation-platform/repomix-output.txt
Normal file
File diff suppressed because it is too large
Load Diff
367
packages/reservation-platform/scripts/generate-data.js
Normal file
367
packages/reservation-platform/scripts/generate-data.js
Normal file
@ -0,0 +1,367 @@
|
||||
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,26 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { BarChart } from "@tremor/react";
|
||||
|
||||
interface AbortReasonsBarChartProps {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: temporary fix
|
||||
data: any[];
|
||||
}
|
||||
|
||||
export function AbortReasonsBarChart(props: AbortReasonsBarChartProps) {
|
||||
const { data } = props;
|
||||
|
||||
const dataFormatter = (number: number) => Intl.NumberFormat("de-DE").format(number).toString();
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
className="mt-6"
|
||||
data={data}
|
||||
index="name"
|
||||
categories={["Anzahl"]}
|
||||
colors={["blue"]}
|
||||
valueFormatter={dataFormatter}
|
||||
yAxisWidth={48}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { DonutChart, Legend } from "@tremor/react";
|
||||
|
||||
const dataFormatter = (number: number) => Intl.NumberFormat("de-DE").format(number).toString();
|
||||
|
||||
interface LoadFactorChartProps {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: temp. fix
|
||||
data: any[];
|
||||
}
|
||||
export function LoadFactorChart(props: LoadFactorChartProps) {
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<DonutChart data={data} variant="donut" colors={["green", "yellow"]} valueFormatter={dataFormatter} />
|
||||
<Legend categories={["Frei", "Belegt"]} colors={["green", "yellow"]} className="max-w-xs" />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from "recharts";
|
||||
|
||||
export const description = "Ein Säulendiagramm zur Darstellung der Abbruchgründe und ihrer Häufigkeit";
|
||||
|
||||
interface AbortReasonCountChartProps {
|
||||
abortReasonCount: {
|
||||
abortReason: string;
|
||||
count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
abortReason: {
|
||||
label: "Abbruchgrund",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export function AbortReasonCountChart({ abortReasonCount }: AbortReasonCountChartProps) {
|
||||
// Transform data to fit the chart structure
|
||||
const chartData = abortReasonCount.map((reason) => ({
|
||||
abortReason: reason.abortReason,
|
||||
count: reason.count,
|
||||
}));
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
"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>
|
||||
);
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
export const description = "Ein Bereichsdiagramm zur Darstellung der prognostizierten Nutzung pro Wochentag";
|
||||
|
||||
interface ForecastData {
|
||||
day: number; // 0 for Sunday, 1 for Monday, ..., 6 for Saturday
|
||||
usageMinutes: number;
|
||||
}
|
||||
|
||||
interface ForecastChartProps {
|
||||
forecastData: ForecastData[];
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
usage: {
|
||||
label: "Prognostizierte Nutzung",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const daysOfWeek = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"];
|
||||
|
||||
export function ForecastPrinterUsageChart({ forecastData }: ForecastChartProps) {
|
||||
// Transform and slice data to fit the chart structure
|
||||
const chartData = forecastData.map((data) => ({
|
||||
//slice(1, forecastData.length - 1).
|
||||
day: daysOfWeek[data.day], // Map day number to weekday name
|
||||
usage: data.usageMinutes,
|
||||
}));
|
||||
|
||||
return (
|
||||
<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(", ");
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
"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>
|
||||
);
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
"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,24 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { DonutChart, Legend } from "@tremor/react";
|
||||
|
||||
const dataFormatter = (number: number) => Intl.NumberFormat("de-DE").format(number).toString();
|
||||
|
||||
interface PrintJobsDonutProps {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: temp. fix
|
||||
data: any[];
|
||||
}
|
||||
export function PrintJobsDonut(props: PrintJobsDonutProps) {
|
||||
const { data } = props;
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<DonutChart data={data} variant="donut" colors={["green", "red", "yellow"]} valueFormatter={dataFormatter} />
|
||||
<Legend
|
||||
categories={["Abgeschlossen", "Abgebrochen", "Ausstehend"]}
|
||||
colors={["green", "red", "yellow"]}
|
||||
className="max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,18 +1,20 @@
|
||||
import { AdminSidebar } from "@/app/admin/admin-sidebar";
|
||||
import { validateRequest } from "@/server/auth";
|
||||
import { UserRole } from "@/server/auth/permissions";
|
||||
import { guard, is_not } from "@/utils/heimdall";
|
||||
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)) {
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,17 @@
|
||||
import { AbortReasonsBarChart } from "@/app/admin/charts/abort-reasons";
|
||||
import { LoadFactorChart } from "@/app/admin/charts/load-factor";
|
||||
import { PrintJobsDonut } from "@/app/admin/charts/printjobs-donut";
|
||||
import { 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 = {
|
||||
@ -14,114 +21,100 @@ export const metadata: Metadata = {
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const allPrintJobs = await db.query.printJobs.findMany({
|
||||
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 totalAmountOfPrintJobs = allPrintJobs.length;
|
||||
|
||||
const now = new Date();
|
||||
const completedPrintJobs = allPrintJobs.filter((job) => {
|
||||
const currentPrintJobs = printJobs.filter((job) => {
|
||||
if (job.aborted) return false;
|
||||
const endAt = new Date(job.startAt).getTime() + job.durationInMinutes * 1000 * 60;
|
||||
return endAt < now.getTime();
|
||||
}).length;
|
||||
const abortedPrintJobs = allPrintJobs.filter((job) => job.aborted).length;
|
||||
const pendingPrintJobs = totalAmountOfPrintJobs - completedPrintJobs - abortedPrintJobs;
|
||||
|
||||
const abortedPrintJobsReasons = Object.entries(
|
||||
allPrintJobs.reduce((accumulator: Record<string, number>, job) => {
|
||||
if (job.aborted && job.abortReason) {
|
||||
if (!accumulator[job.abortReason]) {
|
||||
accumulator[job.abortReason] = 1;
|
||||
} else {
|
||||
accumulator[job.abortReason]++;
|
||||
}
|
||||
}
|
||||
return accumulator;
|
||||
}, {}),
|
||||
).map(([name, count]) => ({ name, Anzahl: count }));
|
||||
const endAt = job.startAt.getTime() + job.durationInMinutes * 1000 * 60;
|
||||
|
||||
const mostAbortedPrinter = allPrintJobs.reduce((prev, current) => (prev.aborted > current.aborted ? prev : current));
|
||||
|
||||
const mostUsedPrinter = allPrintJobs.reduce((prev, current) =>
|
||||
prev.durationInMinutes > current.durationInMinutes ? prev : current,
|
||||
);
|
||||
|
||||
const allPrinters = await db.query.printers.findMany();
|
||||
|
||||
const freePrinters = allPrinters.filter((printer) => {
|
||||
const jobs = allPrintJobs.filter((job) => job.printerId === printer.id);
|
||||
const now = new Date();
|
||||
const inUse = jobs.some((job) => {
|
||||
const endAt = new Date(job.startAt).getTime() + job.durationInMinutes * 1000 * 60;
|
||||
return endAt > now.getTime();
|
||||
});
|
||||
return !inUse;
|
||||
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>
|
||||
{allPrinters.map((printer) => (
|
||||
<TabsTrigger key={printer.id} value={printer.id}>
|
||||
{printer.name}
|
||||
</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">
|
||||
<DataCard title="Drucker mit meisten Reservierungen" value={mostUsedPrinter.printer.name} icon="Printer" />
|
||||
<DataCard title="Drucker mit meisten Abbrüchen" value={mostAbortedPrinter.printer.name} icon="Printer" />
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Druckaufträge</CardTitle>
|
||||
<CardDescription>nach Status</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PrintJobsDonut
|
||||
data={[
|
||||
{ name: "Abgeschlossen", value: completedPrintJobs },
|
||||
{ name: "Abgebrochen", value: abortedPrintJobs },
|
||||
{ name: "Ausstehend", value: pendingPrintJobs },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full ">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Auslastung: <span>{((1 - freePrinters.length / allPrinters.length) * 100).toFixed(2)}%</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LoadFactorChart
|
||||
data={[
|
||||
{ name: "Frei", value: freePrinters.length },
|
||||
{ name: "Belegt", value: allPrinters.length - freePrinters.length },
|
||||
]}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Abgebrochene Druckaufträge nach Abbruchgrund</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AbortReasonsBarChart data={abortedPrintJobsReasons} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div 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>
|
||||
{allPrinters.map((printer) => (
|
||||
<TabsContent key={printer.id} value={printer.id}>
|
||||
{printer.description}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
|
@ -29,7 +29,13 @@ export function DeletePrinterDialog(props: DeletePrinterDialogProps) {
|
||||
description: "Drucker wird gelöscht...",
|
||||
});
|
||||
try {
|
||||
await deletePrinter(printerId);
|
||||
const result = await deletePrinter(printerId);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
toast({
|
||||
description: "Drucker wurde gelöscht.",
|
||||
});
|
||||
|
@ -57,11 +57,17 @@ export function PrinterForm(props: PrinterFormProps) {
|
||||
|
||||
// Update
|
||||
try {
|
||||
await updatePrinter(printer.id, {
|
||||
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);
|
||||
|
||||
@ -90,11 +96,17 @@ export function PrinterForm(props: PrinterFormProps) {
|
||||
|
||||
// Create
|
||||
try {
|
||||
await createPrinter({
|
||||
const result = await createPrinter({
|
||||
description: values.description,
|
||||
name: values.name,
|
||||
status: values.status,
|
||||
});
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
return new Response(fs.readFileSync("./db/sqlite.db"));
|
||||
}
|
||||
|
@ -2,12 +2,19 @@ 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),
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { getPrinters } from "@/server/actions/printers";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const printers = await getPrinters();
|
||||
|
||||
|
@ -7,15 +7,30 @@ import { eq } from "drizzle-orm";
|
||||
import { generateIdFromEntropySize } from "lucia";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
interface GithubEmailResponse {
|
||||
email: string;
|
||||
primary: boolean;
|
||||
verified: boolean;
|
||||
visibility: string;
|
||||
}
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
const storedState = cookies().get("github_oauth_state")?.value ?? null;
|
||||
if (!code || !state || !storedState || state !== storedState) {
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status_text: "Something is wrong",
|
||||
data: { code, state, storedState },
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -27,7 +42,16 @@ export async function GET(request: Request): Promise<Response> {
|
||||
});
|
||||
const githubUser: GitHubUserResult = await githubUserResponse.json();
|
||||
|
||||
// Replace this with your own DB client.
|
||||
// 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),
|
||||
});
|
||||
@ -56,7 +80,10 @@ export async function GET(request: Request): Promise<Response> {
|
||||
|
||||
const session = await lucia.createSession(userId, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||
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: {
|
||||
@ -64,13 +91,18 @@ export async function GET(request: Request): Promise<Response> {
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
// the specific error message depends on the provider
|
||||
if (e instanceof OAuth2RequestError) {
|
||||
// invalid code
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status_text: "Invalid code",
|
||||
error: JSON.stringify(e),
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
|
@ -2,14 +2,18 @@ import { github } from "@/server/auth/oauth";
|
||||
import { generateState } from "arctic";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const state = generateState();
|
||||
const url = await github.createAuthorizationURL(state);
|
||||
const url = await github.createAuthorizationURL(state, {
|
||||
scopes: ["user"],
|
||||
});
|
||||
const ONE_HOUR = 60 * 60;
|
||||
|
||||
cookies().set("github_oauth_state", state, {
|
||||
path: "/",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
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",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 166 KiB |
@ -2,76 +2,60 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--muted: 0 0% 90.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--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: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
--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: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--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: 0 0% 98%;
|
||||
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--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%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
@ -52,7 +52,13 @@ export function CancelForm(props: CancelFormProps) {
|
||||
description: "Druckauftrag wird abgebrochen...",
|
||||
});
|
||||
try {
|
||||
await abortPrintJob(jobId, values.abortReason);
|
||||
const result = await abortPrintJob(jobId, values.abortReason);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
toast({
|
||||
description: "Druckauftrag wurde abgebrochen.",
|
||||
|
@ -17,7 +17,13 @@ export function EditComments(props: EditCommentsProps) {
|
||||
|
||||
const debounced = useDebouncedCallback(async (value) => {
|
||||
try {
|
||||
await updatePrintComments(jobId, value);
|
||||
const result = await updatePrintComments(jobId, value);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
toast({
|
||||
description: "Anmerkungen wurden gespeichert.",
|
||||
});
|
||||
|
@ -53,7 +53,14 @@ export function ExtendForm(props: ExtendFormProps) {
|
||||
description: "Druckauftrag wird verlängert...",
|
||||
});
|
||||
try {
|
||||
await extendPrintJob(jobId, values.minutes, values.hours);
|
||||
const result = await extendPrintJob(jobId, values.minutes, values.hours);
|
||||
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
|
@ -27,7 +27,13 @@ export function FinishForm(props: FinishFormProps) {
|
||||
description: "Druckauftrag wird abgeschlossen...",
|
||||
});
|
||||
try {
|
||||
await earlyFinishPrintJob(jobId);
|
||||
const result = await earlyFinishPrintJob(jobId);
|
||||
if (result?.error) {
|
||||
toast({
|
||||
description: result.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
toast({
|
||||
description: "Druckauftrag wurde abgeschlossen.",
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ export default async function JobDetailsPage(props: JobDetailsPageProps) {
|
||||
});
|
||||
|
||||
if (!jobDetails) {
|
||||
return <div>Job not found</div>;
|
||||
return <div>Druckauftrag wurde nicht gefunden.</div>;
|
||||
}
|
||||
|
||||
const jobIsOnGoing = new Date(jobDetails.startAt).getTime() + jobDetails.durationInMinutes * 60 * 1000 > Date.now();
|
||||
|
@ -1,15 +1,8 @@
|
||||
import { Header } from "@/components/header";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { cn } from "@/utils/styles";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import "@/app/globals.css";
|
||||
import { Inter as FontSans } from "next/font/google";
|
||||
|
||||
const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
@ -23,13 +16,15 @@ 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={cn("min-h-dvh bg-muted font-sans antialiased", fontSans.variable)}>
|
||||
<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}
|
||||
|
11
packages/reservation-platform/src/app/not-found.tsx
Normal file
11
packages/reservation-platform/src/app/not-found.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
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,11 +1,12 @@
|
||||
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, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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 = {
|
||||
@ -43,8 +44,10 @@ export default async function HomePage() {
|
||||
{/* NEEDS TO BE FIXED FOR A NEW / EMPTY USER {isLoggedIn && <PersonalizedCards />} */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Druckerbelegung</CardTitle>
|
||||
<CardDescription>({printers.length} Verfügbar)</CardDescription>
|
||||
<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} />
|
||||
@ -53,8 +56,10 @@ export default async function HomePage() {
|
||||
{userIsLoggedIn && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Druckaufträge</CardTitle>
|
||||
<CardDescription>Deine aktuellen Druckaufträge</CardDescription>
|
||||
<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} />
|
||||
|
@ -1,15 +1,7 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogClose } from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
@ -17,6 +9,7 @@ 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";
|
||||
@ -41,6 +34,7 @@ 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),
|
||||
@ -52,13 +46,25 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
});
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (!isLocked) {
|
||||
setLocked(true);
|
||||
setTimeout(() => {
|
||||
setLocked(false);
|
||||
}, 1000 * 5);
|
||||
} else {
|
||||
toast({
|
||||
description: "Bitte warte ein wenig, bevor du eine weitere Reservierung tätigst...",
|
||||
variant: "default",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.hours === 0 && values.minutes === 0) {
|
||||
form.setError("hours", {
|
||||
message: "",
|
||||
});
|
||||
form.setError("minutes", {
|
||||
message:
|
||||
"Die Dauer des Druckauftrags muss mindestens 1 Minute betragen.",
|
||||
message: "Die Dauer des Druckauftrags muss mindestens 1 Minute betragen.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -70,6 +76,12 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
userId: userId,
|
||||
printerId: printerId,
|
||||
});
|
||||
if (typeof jobId === "object") {
|
||||
toast({
|
||||
description: jobId.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
router.push(`/job/${jobId}`);
|
||||
} catch (error) {
|
||||
@ -128,9 +140,8 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
<Textarea placeholder="" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
In dieses Feld kannst du Anmerkungen zu deinem Druckauftrag
|
||||
hinzufügen. Sie können beispielsweise Informationen über das
|
||||
Druckmaterial, die Druckqualität oder die Farbe enthalten.
|
||||
In dieses Feld kannst du Anmerkungen zu deinem Druckauftrag hinzufügen. Sie können beispielsweise
|
||||
Informationen über das Druckmaterial, die Druckqualität oder die Farbe enthalten.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@ -140,17 +151,14 @@ export function PrinterReserveForm(props: PrinterReserveFormProps) {
|
||||
<If condition={isDialog}>
|
||||
<Then>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="gap-2 flex items-center"
|
||||
>
|
||||
<Button variant={"secondary"} className="gap-2 flex items-center">
|
||||
<XCircleIcon className="w-4 h-4" />
|
||||
<span>Abbrechen</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</Then>
|
||||
</If>
|
||||
<Button type="submit" className="gap-2 flex items-center">
|
||||
<Button type="submit" className="gap-2 flex items-center" disabled={isLocked}>
|
||||
<CalendarPlusIcon className="w-4 h-4" />
|
||||
<span>Reservieren</span>
|
||||
</Button>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HeaderNavigation } from "@/components/header/navigation";
|
||||
import { LoginButton } from "@/components/login-button";
|
||||
import { LogoutButton } from "@/components/logout-button";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { validateRequest } from "@/server/auth";
|
||||
import { UserRole, hasRole } from "@/server/auth/permissions";
|
||||
import { ScanFaceIcon, StickerIcon, UserIcon, WrenchIcon } from "lucide-react";
|
||||
import { StickerIcon, UserIcon, WrenchIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { If, Then } from "react-if";
|
||||
|
||||
@ -78,14 +78,7 @@ export async function Header() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{user == null && (
|
||||
<Button variant={"ghost"} className="gap-2 flex items-center" asChild>
|
||||
<Link href="/auth/login">
|
||||
<ScanFaceIcon className="w-4 h-4" />
|
||||
<span>Anmelden</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{user == null && <LoginButton />}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
@ -1,10 +1,12 @@
|
||||
"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;
|
||||
}
|
||||
|
||||
@ -12,7 +14,8 @@ export function HeaderNavigation() {
|
||||
const pathname = usePathname();
|
||||
const sites: Site[] = [
|
||||
{
|
||||
name: "Mein Dashboard",
|
||||
name: "Dashboard",
|
||||
icon: <LayersIcon className="w-4 h-4" />,
|
||||
path: "/",
|
||||
},
|
||||
/* {
|
||||
@ -21,6 +24,7 @@ export function HeaderNavigation() {
|
||||
}, */
|
||||
{
|
||||
name: "Mein Profil",
|
||||
icon: <ContactRoundIcon className="w-4 h-4" />,
|
||||
path: "/my/profile",
|
||||
},
|
||||
];
|
||||
@ -31,12 +35,13 @@ export function HeaderNavigation() {
|
||||
<Link
|
||||
key={site.path}
|
||||
href={site.path}
|
||||
className={cn("transition-colors hover:text-neutral-50", {
|
||||
"text-neutral-50": pathname === 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.name}
|
||||
{site.icon}
|
||||
<span>{site.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
@ -0,0 +1,37 @@
|
||||
"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,12 +1,21 @@
|
||||
"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={() => logout()} className="flex items-center gap-2">
|
||||
<Link href="/" onClick={onClick} className="flex items-center gap-2">
|
||||
<LogOutIcon className="w-4 h-4" />
|
||||
<span>Abmelden</span>
|
||||
</Link>
|
||||
|
@ -22,29 +22,29 @@ export default async function PersonalizedCards() {
|
||||
.reduce((acc, curr) => acc + curr.durationInMinutes, 0);
|
||||
const averagePrintingHoursPerWeek = totalPrintingMinutes / 60 / 52;
|
||||
|
||||
const mostUsedPrinters = allPrintJobs
|
||||
const mostUsedPrinters = {printer:{name:'-'}}; /*allPrintJobs
|
||||
.map((job) => job.printer.name)
|
||||
.reduce<Record<string, number>>((acc, curr) => {
|
||||
.reduce((acc, curr) => {
|
||||
acc[curr] = (acc[curr] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
}, {});*/
|
||||
|
||||
const mostUsedPrinter = Object.keys(mostUsedPrinters).reduce((a, b) =>
|
||||
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 = allPrintJobs
|
||||
const mostUsedWeekday = {printer:{name:'-'}}; /*allPrintJobs
|
||||
.map((job) => job.startAt.getDay())
|
||||
.reduce<Record<string, number>>((acc, curr) => {
|
||||
.reduce((acc, curr) => {
|
||||
acc[curr] = (acc[curr] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
}, {});*/
|
||||
|
||||
const mostUsedWeekdayIndex = Object.keys(mostUsedWeekday).reduce((a, b) =>
|
||||
const mostUsedWeekdayIndex = ""; /*Object.keys(mostUsedWeekday).reduce((a, b) =>
|
||||
mostUsedWeekday[a] > mostUsedWeekday[b] ? a : b,
|
||||
);
|
||||
);*/
|
||||
|
||||
const mostUsedWeekdayName = new Intl.DateTimeFormat("de-DE", {
|
||||
weekday: "long",
|
||||
|
@ -20,8 +20,6 @@ export function Countdown(props: CountdownProps) {
|
||||
return <>...</>;
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
const days = Math.floor(data.remainingTime / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((data.remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((data.remainingTime % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
@ -33,23 +33,25 @@ export function PrinterCard(props: PrinterCardProps) {
|
||||
"opacity-50 select-none cursor-not-allowed": status === PrinterStatus.OUT_OF_ORDER,
|
||||
})}
|
||||
>
|
||||
<CardHeader className="flex flex-row justify-between">
|
||||
<div>
|
||||
<CardTitle>{printer.name}</CardTitle>
|
||||
<CardDescription>{printer.description}</CardDescription>
|
||||
<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>
|
||||
<Badge
|
||||
className={cn({
|
||||
"bg-green-500 hover:bg-green-400": status === PrinterStatus.IDLE,
|
||||
"bg-red-500 hover:bg-red-500": status === PrinterStatus.OUT_OF_ORDER,
|
||||
"bg-yellow-500 hover:bg-yellow-400": status === PrinterStatus.RESERVED,
|
||||
})}
|
||||
>
|
||||
{status === PrinterStatus.RESERVED && <Countdown jobId={printer.printJobs[0].id} />}
|
||||
<If condition={status === PrinterStatus.RESERVED}>
|
||||
<Else>{translatePrinterStatus(status)}</Else>
|
||||
</If>
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-end">
|
||||
<If condition={status === PrinterStatus.IDLE && userIsLoggedIn && !hasRole(user, UserRole.GUEST)}>
|
||||
|
370
packages/reservation-platform/src/components/ui/chart.tsx
Normal file
370
packages/reservation-platform/src/components/ui/chart.tsx
Normal file
@ -0,0 +1,370 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
import {
|
||||
NameType,
|
||||
Payload,
|
||||
ValueType,
|
||||
} from "recharts/types/component/DefaultTooltipContent"
|
||||
|
||||
import { cn } from "@/utils/styles"
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
}
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"]
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([_, config]) => config.theme || config.color
|
||||
)
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
])
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center"
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center"
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined
|
||||
|
||||
let configLabelKey: string = key
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config]
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { lucia, validateRequest } from "@/server/auth";
|
||||
import { AuthenticationError } from "@/utils/errors";
|
||||
import strings from "@/utils/strings";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
@ -9,13 +9,17 @@ export async function logout(path?: string) {
|
||||
const { session } = await validateRequest();
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError();
|
||||
return {
|
||||
error: strings.ERROR.NO_SESSION,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await lucia.invalidateSession(session.id);
|
||||
} catch (error) {
|
||||
throw new AuthenticationError();
|
||||
return {
|
||||
error: strings.ERROR.NO_SESSION,
|
||||
};
|
||||
}
|
||||
|
||||
const sessionCookie = lucia.createBlankSessionCookie();
|
||||
|
@ -4,8 +4,8 @@ import { validateRequest } from "@/server/auth";
|
||||
import { UserRole } from "@/server/auth/permissions";
|
||||
import { db } from "@/server/db";
|
||||
import { printJobs, users } from "@/server/db/schema";
|
||||
import { PermissionError } from "@/utils/errors";
|
||||
import { IS, guard } from "@/utils/guard";
|
||||
import strings from "@/utils/strings";
|
||||
import { type InferInsertModel, eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
@ -13,7 +13,9 @@ export async function createPrintJob(printJob: InferInsertModel<typeof printJobs
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -22,7 +24,9 @@ export async function createPrintJob(printJob: InferInsertModel<typeof printJobs
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
@ -32,7 +36,9 @@ export async function createPrintJob(printJob: InferInsertModel<typeof printJobs
|
||||
|
||||
return result[0].jobId;
|
||||
} catch (error) {
|
||||
throw new Error("Druckauftrag konnte nicht hinzugefügt werden.");
|
||||
return {
|
||||
error: "Druckauftrag konnte nicht hinzugefügt werden.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,7 +46,9 @@ export async function createPrintJob(printJob: InferInsertModel<typeof printJobs
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, is, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION
|
||||
}
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -49,7 +57,9 @@ export async function createPrintJob(printJob: InferInsertModel<typeof printJobs
|
||||
});
|
||||
|
||||
if (guard(dbUser, is, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION
|
||||
}
|
||||
}
|
||||
|
||||
await db.update(printJobs).set(printJob).where(eq(printJobs.id, jobId));
|
||||
@ -59,7 +69,9 @@ export async function abortPrintJob(jobId: string, reason: string) {
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -68,7 +80,9 @@ export async function abortPrintJob(jobId: string, reason: string) {
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the print job
|
||||
@ -77,22 +91,26 @@ export async function abortPrintJob(jobId: string, reason: string) {
|
||||
});
|
||||
|
||||
if (!printJob) {
|
||||
throw new Error("Druckauftrag nicht gefunden");
|
||||
return {
|
||||
error: "Druckauftrag nicht gefunden",
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the print job is already aborted or completed
|
||||
if (printJob.aborted) {
|
||||
throw new Error("Druckauftrag wurde bereits abgebrochen");
|
||||
return { error: "Druckauftrag wurde bereits abgebrochen" };
|
||||
}
|
||||
|
||||
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
|
||||
throw new Error("Druckauftrag ist bereits abgeschlossen");
|
||||
return { error: "Druckauftrag ist bereits abgeschlossen" };
|
||||
}
|
||||
|
||||
// Check if user is the owner of the print job
|
||||
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
|
||||
if (printJob.userId !== dbUser!.id || dbUser!.role !== UserRole.ADMIN) {
|
||||
throw new PermissionError();
|
||||
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
// Get duration in minutes since startAt
|
||||
@ -115,7 +133,9 @@ export async function earlyFinishPrintJob(jobId: string) {
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -124,7 +144,9 @@ export async function earlyFinishPrintJob(jobId: string) {
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the print job
|
||||
@ -133,22 +155,24 @@ export async function earlyFinishPrintJob(jobId: string) {
|
||||
});
|
||||
|
||||
if (!printJob) {
|
||||
throw new Error("Druckauftrag nicht gefunden");
|
||||
return { error: "Druckauftrag nicht gefunden" };
|
||||
}
|
||||
|
||||
// Check if the print job is already aborted or completed
|
||||
if (printJob.aborted) {
|
||||
throw new Error("Druckauftrag wurde bereits abgebrochen");
|
||||
return { error: "Druckauftrag wurde bereits abgebrochen" };
|
||||
}
|
||||
|
||||
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
|
||||
throw new Error("Druckauftrag ist bereits abgeschlossen");
|
||||
return { error: "Druckauftrag ist bereits abgeschlossen" };
|
||||
}
|
||||
|
||||
// Check if user is the owner of the print job
|
||||
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
|
||||
if (printJob.userId !== dbUser!.id || dbUser!.role !== UserRole.ADMIN) {
|
||||
throw new PermissionError();
|
||||
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
// Get duration in minutes since startAt
|
||||
@ -169,7 +193,9 @@ export async function extendPrintJob(jobId: string, minutes: number, hours: numb
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -178,7 +204,9 @@ export async function extendPrintJob(jobId: string, minutes: number, hours: numb
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the print job
|
||||
@ -187,22 +215,24 @@ export async function extendPrintJob(jobId: string, minutes: number, hours: numb
|
||||
});
|
||||
|
||||
if (!printJob) {
|
||||
throw new Error("Druckauftrag nicht gefunden");
|
||||
return { error: "Druckauftrag nicht gefunden" };
|
||||
}
|
||||
|
||||
// Check if the print job is already aborted or completed
|
||||
if (printJob.aborted) {
|
||||
throw new Error("Druckauftrag wurde bereits abgebrochen");
|
||||
return { error: "Druckauftrag wurde bereits abgebrochen" };
|
||||
}
|
||||
|
||||
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
|
||||
throw new Error("Druckauftrag ist bereits abgeschlossen");
|
||||
return { error: "Druckauftrag ist bereits abgeschlossen" };
|
||||
}
|
||||
|
||||
// Check if user is the owner of the print job
|
||||
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
|
||||
if (printJob.userId !== dbUser!.id || dbUser!.role !== UserRole.ADMIN) {
|
||||
throw new PermissionError();
|
||||
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const duration = minutes + hours * 60;
|
||||
@ -222,7 +252,9 @@ export async function updatePrintComments(jobId: string, comments: string) {
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -231,7 +263,9 @@ export async function updatePrintComments(jobId: string, comments: string) {
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS, UserRole.GUEST)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the print job
|
||||
@ -240,22 +274,24 @@ export async function updatePrintComments(jobId: string, comments: string) {
|
||||
});
|
||||
|
||||
if (!printJob) {
|
||||
throw new Error("Druckauftrag nicht gefunden");
|
||||
return { error: "Druckauftrag nicht gefunden" };
|
||||
}
|
||||
|
||||
// Check if the print job is already aborted or completed
|
||||
if (printJob.aborted) {
|
||||
throw new Error("Druckauftrag wurde bereits abgebrochen");
|
||||
return { error: "Druckauftrag wurde bereits abgebrochen" };
|
||||
}
|
||||
|
||||
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
|
||||
throw new Error("Druckauftrag ist bereits abgeschlossen");
|
||||
return { error: "Druckauftrag ist bereits abgeschlossen" };
|
||||
}
|
||||
|
||||
// Check if user is the owner of the print job
|
||||
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
|
||||
if (printJob.userId !== dbUser!.id || dbUser!.role !== UserRole.ADMIN) {
|
||||
throw new PermissionError();
|
||||
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
await db
|
||||
|
@ -1,11 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { validateRequest } from "@/server/auth";
|
||||
import { UserRole } from "@/server/auth/permissions";
|
||||
import { db } from "@/server/db";
|
||||
import { printers, users } from "@/server/db/schema";
|
||||
import { PermissionError } from "@/utils/errors";
|
||||
import { IS_NOT, guard } from "@/utils/guard";
|
||||
import strings from "@/utils/strings";
|
||||
import { type InferInsertModel, eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
@ -13,7 +12,9 @@ export async function createPrinter(printer: InferInsertModel<typeof printers>)
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -22,17 +23,23 @@ export async function createPrinter(printer: InferInsertModel<typeof printers>)
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
if (!printer) {
|
||||
throw new Error("Druckerdaten sind erforderlich.");
|
||||
return {
|
||||
error: "Druckerdaten sind erforderlich.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await db.insert(printers).values(printer);
|
||||
} catch (error) {
|
||||
throw new Error("Drucker konnte nicht hinzugefügt werden.");
|
||||
return {
|
||||
error: "Drucker konnte nicht hinzugefügt werden.",
|
||||
};
|
||||
}
|
||||
|
||||
revalidatePath("/");
|
||||
@ -42,7 +49,9 @@ export async function updatePrinter(id: string, data: InferInsertModel<typeof pr
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -51,17 +60,23 @@ export async function updatePrinter(id: string, data: InferInsertModel<typeof pr
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
throw new Error("Druckerdaten sind erforderlich.");
|
||||
return {
|
||||
error: "Druckerdaten sind erforderlich.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await db.update(printers).set(data).where(eq(printers.id, id));
|
||||
} catch (error) {
|
||||
throw new Error("Drucker konnte nicht aktualisiert werden.");
|
||||
return {
|
||||
error: "Druckerdaten sind erforderlich.",
|
||||
};
|
||||
}
|
||||
|
||||
revalidatePath("/");
|
||||
@ -71,7 +86,9 @@ export async function deletePrinter(id: string) {
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -80,16 +97,22 @@ export async function deletePrinter(id: string) {
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await db.delete(printers).where(eq(printers.id, id));
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(error.message);
|
||||
return {
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
throw new Error("Ein unbekannter Fehler ist aufgetreten.");
|
||||
return {
|
||||
error: "Ein unbekannter Fehler ist aufgetreten.",
|
||||
};
|
||||
}
|
||||
|
||||
revalidatePath("/");
|
||||
|
@ -4,8 +4,8 @@ import { validateRequest } from "@/server/auth";
|
||||
import { UserRole } from "@/server/auth/permissions";
|
||||
import { db } from "@/server/db";
|
||||
import { users } from "@/server/db/schema";
|
||||
import { PermissionError } from "@/utils/errors";
|
||||
import { IS, IS_NOT, guard } from "@/utils/guard";
|
||||
import strings from "@/utils/strings";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
@ -18,7 +18,9 @@ export async function deleteUser(userId: string, path?: string) {
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -27,7 +29,9 @@ export async function deleteUser(userId: string, path?: string) {
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const targetUser = await db.query.users.findFirst({
|
||||
@ -35,11 +39,15 @@ export async function deleteUser(userId: string, path?: string) {
|
||||
});
|
||||
|
||||
if (!targetUser) {
|
||||
throw new Error("Benutzer nicht gefunden");
|
||||
return {
|
||||
error: "Benutzer nicht gefunden",
|
||||
};
|
||||
}
|
||||
|
||||
if (guard(targetUser, IS, UserRole.ADMIN)) {
|
||||
throw new Error("Kann keinen Admin löschen");
|
||||
return {
|
||||
error: "Admins können nicht gelöscht werden.",
|
||||
};
|
||||
}
|
||||
|
||||
await db.delete(users).where(eq(users.id, userId));
|
||||
|
@ -3,8 +3,8 @@ import { validateRequest } from "@/server/auth";
|
||||
import { UserRole } from "@/server/auth/permissions";
|
||||
import { db } from "@/server/db";
|
||||
import { users } from "@/server/db/schema";
|
||||
import { PermissionError } from "@/utils/errors";
|
||||
import { IS_NOT, guard } from "@/utils/guard";
|
||||
import strings from "@/utils/strings";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import type { z } from "zod";
|
||||
@ -19,7 +19,9 @@ export async function updateUser(userId: string, data: z.infer<typeof formSchema
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -28,7 +30,9 @@ export async function updateUser(userId: string, data: z.infer<typeof formSchema
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
await db.update(users).set(data).where(eq(users.id, userId));
|
||||
|
@ -5,8 +5,8 @@ import { validateRequest } from "@/server/auth";
|
||||
import { UserRole } from "@/server/auth/permissions";
|
||||
import { db } from "@/server/db";
|
||||
import { users } from "@/server/db/schema";
|
||||
import { PermissionError } from "@/utils/errors";
|
||||
import { IS, IS_NOT, guard } from "@/utils/guard";
|
||||
import strings from "@/utils/strings";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import type { z } from "zod";
|
||||
@ -18,7 +18,9 @@ export async function updateUser(userId: string, data: z.infer<typeof formSchema
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -27,7 +29,9 @@ export async function updateUser(userId: string, data: z.infer<typeof formSchema
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
await db.update(users).set(data).where(eq(users.id, userId));
|
||||
@ -42,7 +46,9 @@ export async function deleteUser(userId: string) {
|
||||
const { user } = await validateRequest();
|
||||
|
||||
if (guard(user, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const dbUser = await db.query.users.findFirst({
|
||||
@ -51,7 +57,9 @@ export async function deleteUser(userId: string) {
|
||||
});
|
||||
|
||||
if (guard(dbUser, IS_NOT, UserRole.ADMIN)) {
|
||||
throw new PermissionError();
|
||||
return {
|
||||
error: strings.ERROR.PERMISSION,
|
||||
};
|
||||
}
|
||||
|
||||
const targetUser = await db.query.users.findFirst({
|
||||
@ -59,11 +67,11 @@ export async function deleteUser(userId: string) {
|
||||
});
|
||||
|
||||
if (!targetUser) {
|
||||
throw new Error("Benutzer nicht gefunden");
|
||||
return { error: "Benutzer nicht gefunden" };
|
||||
}
|
||||
|
||||
if (guard(targetUser, IS, UserRole.ADMIN)) {
|
||||
throw new Error("Kann keinen Admin löschen");
|
||||
return { error: "Kann keinen Admin löschen" };
|
||||
}
|
||||
|
||||
await db.delete(users).where(eq(users.id, userId));
|
||||
|
@ -1,19 +1,19 @@
|
||||
import type { UserRole } from "@/server/auth/permissions";
|
||||
import { db } from "@/server/db";
|
||||
import { sessions, users } from "@/server/db/schema";
|
||||
import { env } from "@/utils/env";
|
||||
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
|
||||
import { Lucia, type RegisteredDatabaseUserAttributes, type Session } from "lucia";
|
||||
import { cookies } from "next/headers";
|
||||
import { cache } from "react";
|
||||
|
||||
//@ts-ignore
|
||||
const adapter = new DrizzleSQLiteAdapter(db, sessions, users);
|
||||
|
||||
export const lucia = new Lucia(adapter, {
|
||||
sessionCookie: {
|
||||
expires: false,
|
||||
attributes: {
|
||||
secure: env.RUNTIME_ENVIRONMENT === "prod",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
},
|
||||
},
|
||||
getUserAttributes: (attributes) => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { env } from "@/utils/env";
|
||||
import { GitHub } from "arctic";
|
||||
|
||||
export const github = new GitHub(env.OAUTH.CLIENT_ID, env.OAUTH.CLIENT_SECRET, {
|
||||
export const github = new GitHub(process.env.OAUTH_CLIENT_ID as string, process.env.OAUTH_CLIENT_SECRET as string, {
|
||||
enterpriseDomain: "https://git.i.mercedes-benz.com",
|
||||
});
|
||||
|
||||
|
@ -1,123 +1,8 @@
|
||||
import { env } from "@/utils/env";
|
||||
import * as schema from "@/server/db/schema";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
import { drizzle as drizzleJson } from "drizzle-orm/json-db";
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createClient } from "@libsql/client";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
|
||||
// Stellen sicher, dass DB_PATH tatsächlich gesetzt ist
|
||||
const dbPath = env.DB_PATH || "/app/db/sqlite.db";
|
||||
const jsonDbPath = env.DB_JSON_PATH || "/app/db/db.json";
|
||||
|
||||
// JSON-Fallback-Implementierung
|
||||
class JsonDbAdapter {
|
||||
private data: Record<string, any[]> = {};
|
||||
private dbPath: string;
|
||||
|
||||
constructor(dbPath: string) {
|
||||
this.dbPath = dbPath;
|
||||
this.loadFromDisk();
|
||||
}
|
||||
|
||||
private loadFromDisk() {
|
||||
try {
|
||||
if (fs.existsSync(this.dbPath)) {
|
||||
const content = fs.readFileSync(this.dbPath, 'utf8');
|
||||
this.data = JSON.parse(content);
|
||||
} else {
|
||||
// Ensure directory exists
|
||||
const dir = path.dirname(this.dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
this.saveToFile();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading JSON database:', error);
|
||||
this.data = {};
|
||||
this.saveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
private saveToFile() {
|
||||
try {
|
||||
fs.writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Error saving JSON database:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getTable(table: string) {
|
||||
if (!this.data[table]) {
|
||||
this.data[table] = [];
|
||||
}
|
||||
return {
|
||||
all: () => this.data[table],
|
||||
get: (id: string) => this.data[table].find(item => item.id === id),
|
||||
add: (item: any) => {
|
||||
this.data[table].push(item);
|
||||
this.saveToFile();
|
||||
return item;
|
||||
},
|
||||
update: (id: string, item: any) => {
|
||||
const index = this.data[table].findIndex(i => i.id === id);
|
||||
if (index >= 0) {
|
||||
this.data[table][index] = { ...this.data[table][index], ...item };
|
||||
this.saveToFile();
|
||||
return this.data[table][index];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
delete: (id: string) => {
|
||||
const index = this.data[table].findIndex(i => i.id === id);
|
||||
if (index >= 0) {
|
||||
const deleted = this.data[table].splice(index, 1)[0];
|
||||
this.saveToFile();
|
||||
return deleted;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Versuche SQLite zu laden, mit Fallback auf JSON
|
||||
let db;
|
||||
try {
|
||||
// Versuche SQLite zu initialisieren
|
||||
console.log("Initialisiere SQLite-Datenbank...");
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
// Konfiguriere SQLite für zuverlässigeren Betrieb
|
||||
const sqlite = new Database(dbPath, {
|
||||
// Setze längeres Timeout für Operationen auf langsamen Geräten (RPi)
|
||||
timeout: 30000,
|
||||
// Aktiviere WAL-Modus für höhere Performance
|
||||
journalMode: 'wal',
|
||||
// Verbesserte Fehlerbehandlung
|
||||
verbose: console.error,
|
||||
});
|
||||
|
||||
// Aktiviere Fremdschlüssel-Constraints
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
// Exportiere die Drizzle-Datenbankinstanz
|
||||
db = drizzle(sqlite, { schema });
|
||||
console.log("SQLite-Datenbank erfolgreich initialisiert.");
|
||||
} catch (error) {
|
||||
// Bei Fehler: Fallback auf JSON-Datenbank
|
||||
console.warn(`SQLite-Initialisierung fehlgeschlagen: ${error.message}`);
|
||||
console.warn("Verwende JSON-Fallback-Datenbank...");
|
||||
|
||||
try {
|
||||
const jsonDbAdapter = new JsonDbAdapter(jsonDbPath);
|
||||
db = drizzleJson(jsonDbAdapter, { schema });
|
||||
console.log(`JSON-Datenbank wird verwendet: ${jsonDbPath}`);
|
||||
} catch (jsonError) {
|
||||
console.error("Konnte keine Datenbank initialisieren:", jsonError);
|
||||
throw new Error("Keine Datenbankverbindung möglich.");
|
||||
}
|
||||
}
|
||||
|
||||
// Exportiere die Datenbankinstanz
|
||||
export { db };
|
||||
const sqlite = createClient({
|
||||
url: "file:./db/sqlite.db",
|
||||
});
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
|
@ -1,36 +1,4 @@
|
||||
import { db } from "@/server/db";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { migrate } from "drizzle-orm/libsql/migrator";
|
||||
|
||||
try {
|
||||
// Try to use the SQLite migrator if available
|
||||
const { migrate } = require("drizzle-orm/better-sqlite3/migrator");
|
||||
console.log("Using SQLite migrator...");
|
||||
migrate(db, { migrationsFolder: "./drizzle" });
|
||||
console.log("SQLite migration completed successfully.");
|
||||
} catch (error) {
|
||||
console.warn("SQLite migration failed:", error.message);
|
||||
console.warn("Attempting JSON database initialization...");
|
||||
|
||||
try {
|
||||
// Ensure JSON DB file exists
|
||||
const jsonDbPath = process.env.DB_JSON_PATH || "/app/db/db.json";
|
||||
const jsonDir = path.dirname(jsonDbPath);
|
||||
|
||||
if (!fs.existsSync(jsonDir)) {
|
||||
fs.mkdirSync(jsonDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(jsonDbPath)) {
|
||||
// Initialize with empty schema
|
||||
const emptyDb = {};
|
||||
fs.writeFileSync(jsonDbPath, JSON.stringify(emptyDb, null, 2));
|
||||
console.log("Created empty JSON database structure at", jsonDbPath);
|
||||
} else {
|
||||
console.log("JSON database file already exists at", jsonDbPath);
|
||||
}
|
||||
} catch (jsonError) {
|
||||
console.error("Failed to initialize JSON database:", jsonError);
|
||||
throw new Error("Cannot initialize any database system");
|
||||
}
|
||||
}
|
||||
migrate(db, { migrationsFolder: "./drizzle" });
|
||||
|
@ -0,0 +1,54 @@
|
||||
import type { printJobs } from "@/server/db/schema";
|
||||
import type { InferResultType } from "@/utils/drizzle";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
|
||||
export interface PrinterErrorRate {
|
||||
printerId: string;
|
||||
name: string;
|
||||
errorRate: number; // Error rate as a percentage (0-100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the error rate for print jobs aggregated by printer as a percentage.
|
||||
*
|
||||
* @param pJobs - Array of print job objects.
|
||||
* @returns An array of PrinterErrorRate objects, each containing the printer ID and its error rate.
|
||||
*/
|
||||
export function calculatePrinterErrorRate(
|
||||
pJobs: InferResultType<"printJobs", { printer: true }>[],
|
||||
): PrinterErrorRate[] {
|
||||
if (pJobs.length === 0) {
|
||||
return []; // No jobs, no data.
|
||||
}
|
||||
const printers = pJobs.map((job) => job.printer);
|
||||
|
||||
// Group jobs by printer ID
|
||||
const jobsByPrinter: Record<string, InferSelectModel<typeof printJobs>[]> = pJobs.reduce(
|
||||
(acc, job) => {
|
||||
if (!acc[job.printerId]) {
|
||||
acc[job.printerId] = [];
|
||||
}
|
||||
acc[job.printerId].push(job);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, InferSelectModel<typeof printJobs>[]>,
|
||||
);
|
||||
|
||||
// Calculate the error rate for each printer
|
||||
const printerErrorRates: PrinterErrorRate[] = Object.entries(jobsByPrinter).map(([printerId, jobs]) => {
|
||||
const totalJobs = jobs.length;
|
||||
const abortedJobsCount = jobs.filter((job) => job.aborted).length;
|
||||
const errorRate = (abortedJobsCount / totalJobs) * 100;
|
||||
|
||||
const printer = printers.find((printer) => printer.id === printerId);
|
||||
const printerName = printer ? printer.name : "Unbekannter Drucker";
|
||||
|
||||
return {
|
||||
printerId,
|
||||
name: printerName,
|
||||
errorRate: Number.parseFloat(errorRate.toFixed(2)), // Rounded to two decimal places
|
||||
};
|
||||
});
|
||||
|
||||
return printerErrorRates;
|
||||
}
|
39
packages/reservation-platform/src/utils/analytics/errors.ts
Normal file
39
packages/reservation-platform/src/utils/analytics/errors.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import type { InferResultType } from "@/utils/drizzle";
|
||||
|
||||
export interface AbortReasonCount {
|
||||
abortReason: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the count of each unique abort reason for print jobs.
|
||||
*
|
||||
* @param pJobs - Array of print job objects.
|
||||
* @returns An array of AbortReasonCount objects, each containing the abort reason and its count.
|
||||
*/
|
||||
export function calculateAbortReasonsCount(pJobs: InferResultType<"printJobs">[]): AbortReasonCount[] {
|
||||
if (pJobs.length === 0) {
|
||||
return []; // No jobs, no data.
|
||||
}
|
||||
|
||||
// Filter aborted jobs and count each abort reason
|
||||
const abortReasonsCount = pJobs
|
||||
.filter((job) => job.aborted && job.abortReason) // Consider only aborted jobs with a reason
|
||||
.reduce(
|
||||
(acc, job) => {
|
||||
const reason = job.abortReason || "Unbekannter Grund";
|
||||
if (!acc[reason]) {
|
||||
acc[reason] = 0;
|
||||
}
|
||||
acc[reason]++;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
// Convert the result to an array of AbortReasonCount objects
|
||||
return Object.entries(abortReasonsCount).map(([abortReason, count]) => ({
|
||||
abortReason,
|
||||
count,
|
||||
}));
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import type { InferResultType } from "@/utils/drizzle";
|
||||
|
||||
type UsagePerDay = {
|
||||
day: number; // 0 (Sunday) to 6 (Saturday)
|
||||
usageMinutes: number;
|
||||
};
|
||||
|
||||
function aggregateUsageByDay(jobs: InferResultType<"printJobs">[]): {
|
||||
usageData: UsagePerDay[];
|
||||
earliestDate: Date;
|
||||
latestDate: Date;
|
||||
} {
|
||||
const usagePerDayMap = new Map<number, number>();
|
||||
|
||||
let earliestDate: Date | null = null;
|
||||
let latestDate: Date | null = null;
|
||||
|
||||
for (const job of jobs) {
|
||||
let remainingDuration = job.durationInMinutes;
|
||||
const currentStart = new Date(job.startAt);
|
||||
|
||||
// Update earliest and latest dates
|
||||
if (!earliestDate || currentStart < earliestDate) {
|
||||
earliestDate = new Date(currentStart);
|
||||
}
|
||||
const jobEnd = new Date(currentStart);
|
||||
jobEnd.setMinutes(jobEnd.getMinutes() + job.durationInMinutes);
|
||||
if (!latestDate || jobEnd > latestDate) {
|
||||
latestDate = new Date(jobEnd);
|
||||
}
|
||||
|
||||
while (remainingDuration > 0) {
|
||||
const day = currentStart.getDay();
|
||||
|
||||
// Calculate minutes remaining in the current day
|
||||
const minutesRemainingInDay = (24 - currentStart.getHours()) * 60 - currentStart.getMinutes();
|
||||
const minutesToAdd = Math.min(remainingDuration, minutesRemainingInDay);
|
||||
|
||||
// Update the usage for the current day
|
||||
const usageMinutes = usagePerDayMap.get(day) || 0;
|
||||
usagePerDayMap.set(day, usageMinutes + minutesToAdd);
|
||||
|
||||
// Update remaining duration and move to the next day
|
||||
remainingDuration -= minutesToAdd;
|
||||
currentStart.setDate(currentStart.getDate() + 1);
|
||||
currentStart.setHours(0, 0, 0, 0); // Start at the beginning of the next day
|
||||
}
|
||||
}
|
||||
|
||||
const usageData: UsagePerDay[] = Array.from({ length: 7 }, (_, day) => ({
|
||||
day,
|
||||
usageMinutes: usagePerDayMap.get(day) || 0,
|
||||
}));
|
||||
|
||||
if (earliestDate === null) {
|
||||
earliestDate = new Date();
|
||||
}
|
||||
|
||||
if (latestDate === null) {
|
||||
latestDate = new Date();
|
||||
}
|
||||
|
||||
return { usageData, earliestDate: earliestDate, latestDate: latestDate };
|
||||
}
|
||||
|
||||
function countWeekdays(startDate: Date, endDate: Date): number[] {
|
||||
const countPerDay = Array(7).fill(0);
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setHours(0, 0, 0, 0); // Ensure starting at midnight
|
||||
endDate.setHours(0, 0, 0, 0); // Ensure ending at midnight
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const day = currentDate.getDay();
|
||||
countPerDay[day]++;
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
return countPerDay;
|
||||
}
|
||||
|
||||
export function forecastPrinterUsage(jobs: InferResultType<"printJobs">[]): number[] {
|
||||
const { usageData, earliestDate, latestDate } = aggregateUsageByDay(jobs);
|
||||
|
||||
// Count the number of times each weekday occurs in the data period
|
||||
const weekdaysCount = countWeekdays(earliestDate, latestDate);
|
||||
|
||||
const forecasts: number[] = [];
|
||||
for (const data of usageData) {
|
||||
const dayCount = weekdaysCount[data.day];
|
||||
let usagePrediction = data.usageMinutes / dayCount;
|
||||
if (Number.isNaN(usagePrediction)) {
|
||||
usagePrediction = 0;
|
||||
}
|
||||
forecasts.push(Math.round(usagePrediction));
|
||||
}
|
||||
|
||||
return forecasts;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import type { InferResultType } from "@/utils/drizzle";
|
||||
|
||||
export function calculatePrinterUtilization(jobs: InferResultType<"printJobs", { printer: true }>[]) {
|
||||
const printers = jobs.reduce<Record<string, string>>((acc, job) => {
|
||||
acc[job.printerId] = job.printer.name;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const usedTimePerPrinter: Record<string, number> = jobs.reduce(
|
||||
(acc, job) => {
|
||||
acc[job.printer.id] = (acc[job.printer.id] || 0) + job.durationInMinutes;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
const totalTimeInMinutes = 60 * 35 * 3; // 60 Minutes * 35h * 3 Weeks
|
||||
// 35h Woche, 3 mal in der Woche in TBA
|
||||
|
||||
const printerUtilizationPercentage = Object.keys(usedTimePerPrinter).map((printerId) => {
|
||||
const usedTime = usedTimePerPrinter[printerId];
|
||||
|
||||
return {
|
||||
printerId,
|
||||
name: printers[printerId],
|
||||
utilizationPercentage: usedTime / totalTimeInMinutes,
|
||||
};
|
||||
});
|
||||
|
||||
return printerUtilizationPercentage;
|
||||
}
|
52
packages/reservation-platform/src/utils/analytics/volume.ts
Normal file
52
packages/reservation-platform/src/utils/analytics/volume.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import type { printJobs } from "@/server/db/schema";
|
||||
import { endOfDay, endOfMonth, endOfWeek, startOfDay, startOfMonth, startOfWeek } from "date-fns";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
|
||||
interface PrintVolumes {
|
||||
today: number;
|
||||
thisWeek: number;
|
||||
thisMonth: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of print jobs for today, this week, and this month.
|
||||
*
|
||||
* @param printJobs - Array of print job objects.
|
||||
* @returns An object with counts of print jobs for today, this week, and this month.
|
||||
*/
|
||||
export function calculatePrintVolumes(pJobs: InferSelectModel<typeof printJobs>[]): PrintVolumes {
|
||||
const now = new Date();
|
||||
|
||||
// Define time ranges with week starting on Monday
|
||||
const timeRanges = {
|
||||
today: { start: startOfDay(now), end: endOfDay(now) },
|
||||
thisWeek: {
|
||||
start: startOfWeek(now, { weekStartsOn: 1 }),
|
||||
end: endOfWeek(now, { weekStartsOn: 1 }),
|
||||
},
|
||||
thisMonth: { start: startOfMonth(now), end: endOfMonth(now) },
|
||||
};
|
||||
|
||||
// Initialize counts
|
||||
const volumes: PrintVolumes = {
|
||||
today: 0,
|
||||
thisWeek: 0,
|
||||
thisMonth: 0,
|
||||
};
|
||||
|
||||
// Iterate over print jobs and count based on time ranges
|
||||
for (const job of pJobs) {
|
||||
const jobStart = new Date(job.startAt);
|
||||
if (jobStart >= timeRanges.today.start && jobStart <= timeRanges.today.end) {
|
||||
volumes.today += 1;
|
||||
}
|
||||
if (jobStart >= timeRanges.thisWeek.start && jobStart <= timeRanges.thisWeek.end) {
|
||||
volumes.thisWeek += 1;
|
||||
}
|
||||
if (jobStart >= timeRanges.thisMonth.start && jobStart <= timeRanges.thisMonth.end) {
|
||||
volumes.thisMonth += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return volumes;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Environment variables
|
||||
*/
|
||||
export const env = {
|
||||
RUNTIME_ENVIRONMENT: z.enum(["prod", "dev"]).parse(process.env.RUNTIME_ENVIRONMENT),
|
||||
DB_PATH: process.env.DB_PATH || "db/sqlite.db", // Support environment variable or use default
|
||||
DB_JSON_PATH: process.env.DB_JSON_PATH || "db/db.json", // JSON fallback database path
|
||||
OAUTH: {
|
||||
CLIENT_ID: z.string().parse(process.env.OAUTH_CLIENT_ID),
|
||||
CLIENT_SECRET: z.string().parse(process.env.OAUTH_CLIENT_SECRET),
|
||||
},
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import type { UserRole } from "@/server/auth/permissions";
|
||||
import type { users } from "@/server/db/schema";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import type { RegisteredDatabaseUserAttributes } from "lucia";
|
||||
|
||||
// Helpers to improve readability of the guard function
|
||||
export const is = false;
|
||||
export const is_not = true;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function guard(
|
||||
user: RegisteredDatabaseUserAttributes | InferSelectModel<typeof users> | undefined | null,
|
||||
negate: boolean,
|
||||
roleRequirements: UserRole | UserRole[],
|
||||
) {
|
||||
if (!user) {
|
||||
return true; // Guard against unauthenticated users
|
||||
}
|
||||
|
||||
const hasRole = Array.isArray(roleRequirements)
|
||||
? roleRequirements.includes(user?.role as UserRole)
|
||||
: user?.role === roleRequirements;
|
||||
|
||||
return negate ? !hasRole : hasRole;
|
||||
}
|
||||
|
||||
export class PermissionError extends Error {
|
||||
constructor() {
|
||||
super("Du besitzt nicht die erforderlichen Berechtigungen um diese Aktion auszuführen.");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user