9 Commits
main ... test

Author SHA1 Message Date
5fbb2fb4b5 Merge pull request 'das war WSID upgrade' (#5) from better-design into test
Reviewed-on: #5
2026-01-06 00:02:42 +00:00
a4c64b5398 test 2026-01-06 01:02:16 +01:00
cb110a184b Merge pull request 'better-design' (#4) from better-design into test
Reviewed-on: #4
2026-01-05 23:49:51 +00:00
99b89bcabe ich weis nicht mehr 2026-01-06 00:40:54 +01:00
895c55399f wsid update 2025-12-30 20:29:59 +01:00
ee7c866616 Merge pull request 'design update' (#3) from better-design into test
Reviewed-on: #3
2025-12-30 00:31:06 +00:00
5717612db5 design update 2025-12-30 01:27:00 +01:00
3f8fce3c02 Merge pull request 'woms 3.0' (#1) from appwrite-verbindung into test
Reviewed-on: #1
2025-12-29 21:33:27 +00:00
Basilosaurusrex
0e19df6895 woms 3.0 2025-12-29 22:28:43 +01:00
109 changed files with 9260 additions and 32733 deletions

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Dependencies
node_modules/
# Environment variables
.env
.env.local
.env.production
# Build output
dist/
build/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*

116
ADMIN_PANEL_SETUP.md Normal file
View File

@@ -0,0 +1,116 @@
# Admin-Panel Setup
Das Admin-Panel wurde erfolgreich erstellt! Hier ist die Anleitung zur Einrichtung.
## Was wurde erstellt:
1. **Admin-Panel Seite** (`/admin`) - Verwaltung der Dropdown-Optionen
2. **useAdminConfig Hook** - Lädt und speichert die Konfiguration
3. **Navigation erweitert** - Admin-Link erscheint für Admin-Benutzer
4. **CreateTicketModal angepasst** - Verwendet jetzt die konfigurierten Werte
## Schritt 1: Config Collection in Appwrite erstellen
1. Gehe zu `https://appwrite.webklar.com`
2. Öffne dein Projekt **woms**
3. Gehe zu **Databases****woms-database**
4. Klicke auf **Create Collection**
5. **Collection ID**: `config`
6. **Name**: `Admin Configuration`
### Attribute hinzufügen:
| Attribut Name | Typ | Größe | Required | Array |
|--------------|-----|-------|----------|-------|
| `ticketTypes` | string | - | - | ✓ |
| `systems` | string | - | - | ✓ |
| `responseLevels` | string | - | - | ✓ |
| `serviceTypes` | string | - | - | ✓ |
| `priorities` | string | - | - | ✓ |
**Wichtig:** Alle Attribute müssen als **Array** konfiguriert sein!
### Berechtigungen:
- **Read**: `Any` (damit alle Benutzer die Optionen sehen können)
- **Create**: `Users` (nur eingeloggte Benutzer können erstellen)
- **Update**: `Users` (nur eingeloggte Benutzer können aktualisieren)
- **Delete**: `Users` (optional)
## Schritt 2: Admin-Berechtigung konfigurieren
Aktuell wird ein Benutzer als Admin erkannt, wenn:
- Die Email `admin` enthält, ODER
- Die Email `kenso@webklar.com` ist, ODER
- Der Benutzer ein `admin` Label hat
### Option A: Email-basierte Erkennung (aktuell implementiert)
Die Admin-Erkennung erfolgt in:
- `src/pages/AdminPage.jsx` (Zeile ~10)
- `src/components/Navbar.jsx` (Zeile ~29)
Du kannst die Bedingung anpassen:
```javascript
const isAdmin = user?.email?.includes('admin') ||
user?.email === 'deine@email.com' ||
user?.labels?.includes('admin')
```
### Option B: Appwrite Labels verwenden
1. Gehe zu **Auth****Users** im Appwrite Dashboard
2. Wähle einen Benutzer aus
3. Füge ein **Label** hinzu: `admin`
4. Die Admin-Erkennung funktioniert dann automatisch
## Schritt 3: Admin-Panel verwenden
1. **Logge dich als Admin ein**
2. Gehe zu **Admin** in der Navigation (erscheint nur für Admins)
3. Bearbeite die Dropdown-Optionen:
- **Work Order Types** - Ticket-Typen
- **Affected Systems** - Betroffene Systeme
- **Response Levels** - Response-Ebenen
- **Service Types** - Service-Typen
- **Priorities** - Prioritäten (mit Value und Label)
4. Klicke auf **Konfiguration speichern**
## Schritt 4: Testen
1. **Als Admin:**
- Öffne das Admin-Panel
- Ändere einige Optionen
- Speichere die Konfiguration
2. **Als normaler Benutzer:**
- Erstelle ein neues Ticket
- Die geänderten Optionen sollten jetzt in den Dropdowns erscheinen
## Funktionsweise
- **Admin-Panel**: Lädt die Konfiguration aus Appwrite und erlaubt Bearbeitung
- **CreateTicketModal**: Lädt die Konfiguration automatisch und verwendet die Werte
- **Fallback**: Falls die Config nicht geladen werden kann, werden Standard-Werte verwendet
## Troubleshooting
### "Collection config not found"
- Stelle sicher, dass die Collection `config` erstellt wurde
- Überprüfe die Collection ID (muss genau `config` sein)
### "Zugriff verweigert"
- Überprüfe die Admin-Erkennung in `AdminPage.jsx`
- Stelle sicher, dass deine Email den Admin-Kriterien entspricht
### "Konfiguration wird nicht geladen"
- Überprüfe die Berechtigungen der `config` Collection
- Stelle sicher, dass **Read** auf `Any` oder `Users` gesetzt ist
### "Änderungen werden nicht übernommen"
- Überprüfe die **Update** Berechtigung der `config` Collection
- Stelle sicher, dass du als Admin eingeloggt bist
Viel Erfolg! 🚀

74
ADMIN_ROLE_SETUP.md Normal file
View File

@@ -0,0 +1,74 @@
# Admin-Rolle in Appwrite einrichten
Die Admin-Erkennung verwendet jetzt die **Labels** aus Appwrite. Ein Benutzer ist Admin, wenn er das Label `admin` hat.
## Schritt 1: Admin-Label in Appwrite hinzufügen
### Option A: Über das Appwrite Dashboard
1. Gehe zu `https://appwrite.webklar.com`
2. Öffne dein Projekt **woms**
3. Gehe zu **Auth****Users**
4. Wähle den Benutzer aus, der Admin werden soll
5. Klicke auf **Edit** oder öffne die Benutzer-Details
6. Scrolle zu **Labels** (oder **Custom Attributes**)
7. Klicke auf **Add Label** oder **+**
8. Gib `admin` ein
9. Klicke auf **Save** oder **Update**
### Option B: Über die Appwrite API (für mehrere Benutzer)
Du kannst auch Labels programmatisch hinzufügen, aber das ist normalerweise nicht nötig.
## Schritt 2: Überprüfen
Nach dem Hinzufügen des Labels:
1. **Logge dich aus** und wieder **ein** (damit die Session aktualisiert wird)
2. Oder lade die Seite neu (F5)
3. Der **Admin**-Link sollte jetzt in der Navigation erscheinen
4. Du solltest Zugriff auf das Admin-Panel haben
## Schritt 3: Mehrere Admins hinzufügen
Um weitere Admins hinzuzufügen:
1. Wiederhole Schritt 1 für jeden Benutzer
2. Füge das Label `admin` hinzu
3. Die Benutzer müssen sich neu einloggen
## Wie es funktioniert
- **AuthContext**: Prüft ob `user.labels` das Label `admin` enthält
- **Navbar**: Zeigt den Admin-Link nur an, wenn `isAdmin === true`
- **AdminPage**: Blockiert den Zugriff, wenn der Benutzer kein Admin ist
## Debugging
Falls der Admin-Link nicht erscheint:
1. **Browser-Konsole öffnen** (F12)
2. Prüfe das User-Objekt:
```javascript
// In der Browser-Konsole:
console.log(user)
// Prüfe ob user.labels das Array ['admin'] enthält
```
3. **Überprüfe in Appwrite:**
- Gehe zu **Auth** → **Users**
- Öffne deinen Benutzer
- Stelle sicher, dass das Label `admin` vorhanden ist
4. **Session aktualisieren:**
- Logge dich aus und wieder ein
- Oder lade die Seite neu
## Wichtig
- Das Label muss genau `admin` heißen (kleingeschrieben)
- Der Benutzer muss sich nach dem Hinzufügen des Labels neu einloggen
- Labels sind case-sensitive
Viel Erfolg! 🚀

View File

@@ -0,0 +1,102 @@
# Indexes und Berechtigungen für workorders Collection
Diese Anleitung zeigt dir, wie du die Indexes und Berechtigungen für deine `workorders` Collection einrichtest.
## Deine Collection-Informationen
- **Collection ID**: `6943bf7d001901baa60c`
- **Database ID**: `6943bf0e0003291f8c35`
## Schritt 1: Indexes erstellen
Indexes verbessern die Performance beim Filtern und Sortieren. Gehe zu deiner `workorders` Collection im Appwrite Dashboard:
1. Öffne dein Appwrite-Projekt
2. Gehe zu **Databases****woms-database****workorders**
3. Klicke auf den Tab **Indexes**
4. Klicke auf **Create Index**
Erstelle folgende Indexes:
### Index 1: Status
- **Key**: `status`
- **Type**: `key`
- **Attributes**: `status`
- **Order**: `ASC` (aufsteigend)
### Index 2: Priority
- **Key**: `priority`
- **Type**: `key`
- **Attributes**: `priority`
- **Order**: `ASC` (aufsteigend)
### Index 3: Type
- **Key**: `type`
- **Type**: `key`
- **Attributes**: `type`
- **Order**: `ASC` (aufsteigend)
### Index 4: Customer ID
- **Key**: `customerId`
- **Type**: `key`
- **Attributes**: `customerId`
- **Order**: `ASC` (aufsteigend)
### Index 5: Assigned To
- **Key**: `assignedTo`
- **Type**: `key`
- **Attributes**: `assignedTo`
- **Order**: `ASC` (aufsteigend)
### Index 6: Created At
- **Key**: `createdAt`
- **Type**: `key`
- **Attributes**: `createdAt`
- **Order**: `DESC` (absteigend)
## Schritt 2: Berechtigungen (Permissions) einrichten
1. Bleibe in deiner `workorders` Collection
2. Klicke auf den Tab **Settings**
3. Scrolle zu **Permissions**
### Read (Lesen)
- Klicke auf **Add Role** unter "Read"
- Wähle **Any** (wenn alle Tickets sichtbar sein sollen) ODER **Users** (wenn nur eingeloggte Benutzer sehen sollen)
- **Empfehlung**: `Users` für mehr Sicherheit
### Create (Erstellen)
- Klicke auf **Add Role** unter "Create"
- Wähle **Users** (nur eingeloggte Benutzer können Tickets erstellen)
### Update (Aktualisieren)
- Klicke auf **Add Role** unter "Update"
- Wähle **Users** (nur eingeloggte Benutzer können Tickets aktualisieren)
### Delete (Löschen)
- Klicke auf **Add Role** unter "Delete"
- Wähle **Users** (nur eingeloggte Benutzer können Tickets löschen)
## Schritt 3: Überprüfung
Nach dem Einrichten solltest du:
1. ✅ 6 Indexes in der Collection sehen
2. ✅ 4 Berechtigungen (Read, Create, Update, Delete) konfiguriert haben
## Wichtig: Authentication aktivieren
Bevor die Berechtigungen funktionieren, musst du Authentication aktivieren:
1. Gehe zu **Auth** im linken Menü
2. Stelle sicher, dass **Email/Password** aktiviert ist
3. Falls nicht, klicke auf **Create** und aktiviere **Email/Password**
## Testen
Nach der Einrichtung:
1. Starte deine App neu: `npm run dev`
2. Erstelle einen Benutzer oder logge dich ein
3. Versuche ein Ticket zu erstellen - es sollte funktionieren!
Viel Erfolg! 🚀

View File

@@ -0,0 +1,81 @@
# Appwrite Platform-Konfiguration - WICHTIG!
Der Fehler "Project with the requested ID could not be found" kann auch auftreten, wenn die **Platform nicht konfiguriert** ist!
## Schritt 1: Platform in Appwrite konfigurieren
1. Gehe zu [https://cloud.appwrite.io](https://cloud.appwrite.io)
2. Öffne dein Projekt **woms**
3. Gehe zu **Settings****Platforms** (oder **Auth****Settings**)
4. Klicke auf **Add Platform**
5. Wähle **Web App**
6. Gib einen Namen ein (z.B. "WOMS Web")
7. **WICHTIG:** Füge die folgenden Hosts hinzu:
- `localhost` (für Development)
- `127.0.0.1` (für Development)
- Deine Produktions-Domain (falls vorhanden)
8. Klicke auf **Create**
## Schritt 2: Email/Password Auth aktivieren
1. Gehe zu **Auth****Providers** (oder **Settings****Auth**)
2. Suche nach **Email/Password**
3. Aktiviere es (grüner Schalter)
4. Stelle sicher, dass es **aktiviert** ist
## Schritt 3: Project ID nochmal überprüfen
1. Gehe zu **Settings****General**
2. Kopiere die **Project ID** erneut
3. Vergleiche sie mit deiner `.env` Datei
Die Project ID sollte so aussehen: `693d9f37000b35267f1b`
## Schritt 4: Dev-Server neu starten
```bash
# Stoppe den Server (Ctrl+C)
npm run dev
```
## Schritt 5: Browser-Konsole überprüfen
Nach dem Neustart solltest du in der Browser-Konsole sehen:
```
🔧 Appwrite Konfiguration:
Endpoint: https://cloud.appwrite.io/v1
Project ID: 693d9f37000b35267f1b
Database ID: 6943bf0e0003291f8c35
```
## Häufige Probleme
### Problem: "Project with the requested ID could not be found"
**Lösung:**
1. Überprüfe, ob die Platform konfiguriert ist (siehe Schritt 1)
2. Überprüfe, ob `localhost` als Host hinzugefügt wurde
3. Überprüfe die Project ID nochmal im Dashboard
### Problem: "createEmailPasswordSession is not a function"
**Lösung:**
- Ich habe den Code bereits aktualisiert zu `createEmailSession`
- Stelle sicher, dass der Dev-Server neu gestartet wurde
### Problem: "401 Unauthorized" oder "Invalid credentials"
**Lösung:**
1. Stelle sicher, dass Email/Password Auth aktiviert ist
2. Erstelle einen Benutzer im Appwrite Dashboard oder über die Registrierung
## Testen
1. Öffne die App: `http://localhost:5173`
2. Versuche dich zu registrieren oder einzuloggen
3. Schaue in die Browser-Konsole auf Fehlermeldungen
Viel Erfolg! 🚀

86
APPWRITE_SELF_HOSTED.md Normal file
View File

@@ -0,0 +1,86 @@
# Appwrite Self-Hosted Konfiguration
Deine Appwrite-Instanz läuft auf einer eigenen Domain mit Version 1.5.7.
## Konfiguration
### Endpoint
- **URL**: `https://appwrite.webklar.com/v1`
- **Version**: 1.5.7
### .env Datei
```env
VITE_APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1
VITE_APPWRITE_PROJECT_ID=693d9f37000b35267f1b
VITE_APPWRITE_DATABASE_ID=6943bf0e0003291f8c35
```
## Wichtige Unterschiede zu Appwrite Cloud
### 1. Platform-Konfiguration
Bei selbst gehosteten Instanzen muss die Platform möglicherweise anders konfiguriert werden:
1. Gehe zu deinem Appwrite Dashboard: `https://appwrite.webklar.com`
2. Öffne dein Projekt **woms**
3. Gehe zu **Settings****Platforms**
4. Füge eine **Web App** Platform hinzu
5. Wichtig: Füge die folgenden Hosts hinzu:
- `localhost`
- `127.0.0.1`
- Deine Produktions-Domain (falls vorhanden)
### 2. CORS-Einstellungen
Stelle sicher, dass CORS für deine Domain konfiguriert ist:
- `http://localhost:5173` (Development)
- Deine Produktions-Domain
### 3. SSL-Zertifikat
Stelle sicher, dass `https://appwrite.webklar.com` ein gültiges SSL-Zertifikat hat.
## API-Kompatibilität
Appwrite 1.5.7 sollte mit dem Appwrite SDK 13.0 kompatibel sein. Die verwendeten Methoden:
- `account.createEmailSession(email, password)` - für Login
- `account.create(ID.unique(), email, password, name)` - für Registrierung
- `account.get()` - für Session-Check
## Fehlerbehebung
### "Project with the requested ID could not be found"
1. Überprüfe, ob die Platform konfiguriert ist (siehe oben)
2. Überprüfe, ob CORS korrekt konfiguriert ist
3. Überprüfe die Project ID nochmal im Dashboard
### CORS-Fehler
Wenn du CORS-Fehler siehst:
1. Überprüfe die CORS-Einstellungen in deiner Appwrite-Konfiguration
2. Stelle sicher, dass `localhost:5173` erlaubt ist
### SSL-Zertifikat-Fehler
Wenn es SSL-Probleme gibt:
1. Überprüfe, ob das Zertifikat gültig ist
2. Stelle sicher, dass die Domain korrekt konfiguriert ist
## Dev-Server neu starten
Nach Änderungen an der `.env` Datei:
```bash
# Stoppe den Server (Ctrl+C)
npm run dev
```
## Testen
1. Öffne die App: `http://localhost:5173`
2. Überprüfe die Browser-Konsole - du solltest sehen:
```
🔧 Appwrite Konfiguration:
Endpoint: https://appwrite.webklar.com/v1
Project ID: 693d9f37000b35267f1b
Database ID: 6943bf0e0003291f8c35
```
3. Versuche dich zu registrieren oder einzuloggen
Viel Erfolg! 🚀

200
APPWRITE_SETUP.md Normal file
View File

@@ -0,0 +1,200 @@
# Appwrite Setup-Anleitung für WOMS 2.0
Diese Anleitung führt dich Schritt für Schritt durch die Einrichtung von Appwrite für dein Ticket-System.
## Schritt 1: Appwrite Account erstellen
1. Gehe zu [https://cloud.appwrite.io](https://cloud.appwrite.io)
2. Erstelle einen kostenlosen Account oder logge dich ein
3. Erstelle ein neues Projekt (z.B. "WOMS" oder "Ticket-System")
## Schritt 2: Projekt-Konfiguration
1. Öffne dein Projekt im Appwrite Dashboard
2. Gehe zu **Settings****General**
3. Kopiere deine **Project ID** (wird später in der `.env` Datei benötigt)
## Schritt 3: Datenbank erstellen
1. Gehe zu **Databases** im linken Menü
2. Klicke auf **Create Database**
3. Name: `woms-database` (oder ein anderer Name - muss dann in `.env` angepasst werden)
4. Kopiere die **Database ID** (wird später in der `.env` Datei benötigt)
## Schritt 4: Collections erstellen
### Collection 1: `workorders`
1. Klicke auf **Create Collection**
2. Collection ID: `workorders`
3. Name: `Work Orders`
**Attribute hinzufügen:**
| Attribut Name | Typ | Größe | Required | Array | Default |
|--------------|-----|-------|----------|-------|---------|
| `topic` | string | 255 | ✓ | - | - |
| `title` | string | 255 | - | - | - |
| `details` | string | 10000 | - | - | - |
| `description` | string | 5000 | - | - | - |
| `status` | string | 50 | ✓ | - | `Open` |
| `priority` | integer | - | ✓ | - | `1` |
| `type` | string | 50 | - | - | - |
| `systemType` | string | 50 | - | - | - |
| `responseLevel` | string | 50 | - | - | - |
| `serviceType` | string | 50 | - | - | `Remote` |
| `customerId` | string | 50 | - | - | - |
| `customerName` | string | 255 | - | - | - |
| `customerLocation` | string | 255 | - | - | - |
| `assignedTo` | string | 50 | - | - | - |
| `assignedName` | string | 255 | - | - | - |
| `requestedBy` | string | 255 | - | - | - |
| `requestedFor` | string | 255 | - | - | - |
| `startDate` | string | 50 | - | - | - |
| `startTime` | string | 10 | - | - | - |
| `deadline` | string | 50 | - | - | - |
| `endTime` | string | 10 | - | - | - |
| `estimate` | string | 50 | - | - | - |
| `mailCopyTo` | string | 255 | - | - | - |
| `sendNotification` | boolean | - | - | - | `false` |
| `approvalStatus` | string | 50 | - | - | - |
| `woid` | string | 10 | - | - | - |
| `createdAt` | datetime | - | - | - | - |
**Hinweis zu WOID:**
- `woid` = Work Order ID (5-stellig, z.B. "10000", "10001", etc.)
- Wird automatisch vom System generiert
- **WSID (Work Sheet ID)** ist NICHT Teil dieser Collection - siehe `worksheets` Collection!
**Indexes erstellen:**
- `status` (ASC)
- `priority` (ASC)
- `type` (ASC)
- `customerId` (ASC)
- `assignedTo` (ASC)
- `woid` (ASC) - für schnelle WOID-Suche
- `wsid` (ASC) - für schnelle WSID-Suche
- `createdAt` (DESC)
**Berechtigungen (Permissions):**
- **Read**: `Any` (oder `Users` wenn nur eingeloggte Benutzer sehen sollen)
- **Create**: `Users` (nur eingeloggte Benutzer können erstellen)
- **Update**: `Users` (nur eingeloggte Benutzer können aktualisieren)
- **Delete**: `Users` (nur eingeloggte Benutzer können löschen)
### Collection 2: `customers` (optional, für zukünftige Features)
1. Klicke auf **Create Collection**
2. Collection ID: `customers`
3. Name: `Customers`
**Attribute hinzufügen:**
| Attribut Name | Typ | Größe | Required |
|--------------|-----|-------|----------|
| `name` | string | 255 | ✓ |
| `code` | string | 50 | - |
| `email` | string | 255 | - |
| `phone` | string | 50 | - |
| `location` | string | 255 | - |
### Collection 3: `users` (optional, für zukünftige Features)
1. Klicke auf **Create Collection**
2. Collection ID: `users`
3. Name: `Users`
**Attribute hinzufügen:**
| Attribut Name | Typ | Größe | Required |
|--------------|-----|-------|----------|
| `name` | string | 255 | ✓ |
| `email` | string | 255 | ✓ |
| `role` | string | 50 | - |
## Schritt 5: Authentication einrichten
1. Gehe zu **Auth** im linken Menü
2. Aktiviere **Email/Password** als Auth-Methode
3. (Optional) Aktiviere weitere Auth-Methoden wie Google, GitHub, etc.
## Schritt 6: Storage Bucket (optional, für Datei-Uploads)
1. Gehe zu **Storage** im linken Menü
2. Klicke auf **Create Bucket**
3. Bucket ID: `woms-attachments`
4. Name: `WOMS Attachments`
5. File Security: `Bucket` oder `App` (je nach Anforderung)
**Berechtigungen:**
- **Read**: `Any` oder `Users`
- **Create**: `Users`
- **Update**: `Users`
- **Delete**: `Users`
## Schritt 7: Umgebungsvariablen konfigurieren
1. Erstelle eine `.env` Datei im Root-Verzeichnis deines Projekts:
```env
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=deine-project-id-hier
VITE_APPWRITE_DATABASE_ID=woms-database
```
2. Ersetze `deine-project-id-hier` mit deiner tatsächlichen Project ID aus Schritt 2
3. Ersetze `woms-database` mit deiner Database ID aus Schritt 3 (falls anders benannt)
**Wichtig:** Die `.env` Datei sollte **NICHT** in Git committed werden. Sie ist bereits in `.gitignore` aufgeführt.
## Schritt 8: App starten
1. Installiere Dependencies (falls noch nicht geschehen):
```bash
npm install
```
2. Starte den Development-Server:
```bash
npm run dev
```
3. Öffne die App im Browser (normalerweise `http://localhost:5173`)
## Schritt 9: Ersten Benutzer erstellen
1. Gehe zur Login-Seite
2. Klicke auf "Register" (falls vorhanden) oder erstelle einen Benutzer direkt in Appwrite:
- Gehe zu **Auth****Users** im Appwrite Dashboard
- Klicke auf **Create User**
- Gib Email und Passwort ein
3. Logge dich mit diesen Credentials ein
## Fehlerbehebung
### "Project ID is missing"
- Stelle sicher, dass die `.env` Datei existiert und die richtige `VITE_APPWRITE_PROJECT_ID` enthält
- Starte den Dev-Server neu nach Änderungen an der `.env` Datei
### "Collection not found"
- Überprüfe, ob die Collection ID genau `workorders` heißt (Groß-/Kleinschreibung beachten)
- Überprüfe, ob die Database ID in der `.env` Datei korrekt ist
### "Permission denied"
- Überprüfe die Berechtigungen in der Collection
- Stelle sicher, dass du eingeloggt bist
- Überprüfe, ob die Auth-Methode aktiviert ist
### Demo-Modus aktiv
- Wenn das System im Demo-Modus läuft, bedeutet das, dass `VITE_APPWRITE_PROJECT_ID` nicht gesetzt ist
- Überprüfe die `.env` Datei und starte den Server neu
## Nächste Schritte
Nach erfolgreicher Einrichtung kannst du:
- Tickets erstellen, bearbeiten und löschen
- Status und Priorität ändern
- Tickets filtern und durchsuchen
- (Optional) Dateien zu Tickets hochladen
Viel Erfolg! 🚀

63
COLLECTION_ID_FIX.md Normal file
View File

@@ -0,0 +1,63 @@
# Collection ID Problem behoben
## Problem
Die Fehlermeldung "Collection with the requested ID could not be found" trat auf, weil:
1. **Falsche Collection ID verwendet**: Der Code verwendete `'workorders'` (Name) statt der tatsächlichen Collection ID
2. **Query-Syntax für Arrays**: `Query.equal()` wurde mit Arrays verwendet, was in Appwrite 1.5.7 nicht funktioniert
## Lösung
### 1. Collection ID korrigiert
Die Collection ID wurde in `src/lib/appwrite.js` aktualisiert:
```javascript
export const COLLECTIONS = {
WORKORDERS: '6943bf7d001901baa60c', // Echte Collection ID
// ...
}
```
### 2. Query-Syntax für Arrays angepasst
In `src/hooks/useWorkorders.js` wurde die Query-Syntax für Arrays korrigiert:
- **Vorher**: `Query.equal('status', ['Open', 'Occupied'])`
- **Jetzt**: `Query.or([Query.equal('status', 'Open'), Query.equal('status', 'Occupied')])`
## Überprüfung
Nach dem Neustart des Dev-Servers solltest du in der Browser-Konsole sehen:
```
📋 Fetching workorders:
Database ID: 6943bf0e0003291f8c35
Collection ID: 6943bf7d001901baa60c
Queries: 3
```
## Wenn die Collection immer noch nicht gefunden wird
1. **Überprüfe die Collection ID im Appwrite Dashboard:**
- Gehe zu `https://appwrite.webklar.com`
- Öffne dein Projekt **woms**
- Gehe zu **Databases****woms-database****workorders**
- Kopiere die **Collection ID** aus den Settings
- Vergleiche sie mit der ID in `src/lib/appwrite.js`
2. **Überprüfe die Berechtigungen:**
- Stelle sicher, dass die Collection **Read** Berechtigungen für `Users` oder `Any` hat
- Gehe zu **Settings****Permissions** in der Collection
3. **Überprüfe, ob die Collection existiert:**
- Stelle sicher, dass die Collection tatsächlich erstellt wurde
- Überprüfe, ob sie in der richtigen Datenbank ist
## Dev-Server neu starten
```bash
# Stoppe den Server (Ctrl+C)
npm run dev
```
Viel Erfolg! 🚀

View File

@@ -0,0 +1,75 @@
# Config Collection Setup
Die `config` Collection wird für das Admin-Panel benötigt, um die Dropdown-Optionen zu speichern.
## Collection erstellen
1. Gehe zu `https://appwrite.webklar.com`
2. Öffne dein Projekt **woms**
3. Gehe zu **Databases****woms-database**
4. Klicke auf **Create Collection**
5. **Collection ID**: `config`
6. **Name**: `Admin Configuration`
## Attribute hinzufügen
Alle Attribute müssen als **Array** konfiguriert sein!
| Attribut Name | Typ | Größe | Required | Array |
|--------------|-----|-------|----------|-------|
| `ticketTypes` | string | - | - | ✓ |
| `systems` | string | - | - | ✓ |
| `responseLevels` | string | - | - | ✓ |
| `serviceTypes` | string | - | - | ✓ |
| `priorities` | string | - | - | ✓ |
**Wichtig:**
- `priorities` sollte als JSON-String gespeichert werden (Array von Objekten mit `value` und `label`)
- Alle anderen sind einfache String-Arrays
## Berechtigungen
- **Read**: `Any` (damit alle Benutzer die Optionen sehen können)
- **Create**: `Users` (nur eingeloggte Benutzer können erstellen)
- **Update**: `Users` (nur eingeloggte Benutzer können aktualisieren)
- **Delete**: `Users` (optional)
## Erste Config erstellen
Nach dem Erstellen der Collection kannst du:
1. Zum Admin-Panel gehen
2. Die Dropdown-Optionen bearbeiten
3. Auf "Konfiguration speichern" klicken
4. Das System erstellt automatisch das erste Config-Dokument mit der ID `config`
## Alternative: Manuell erstellen
Falls du die Config manuell erstellen möchtest:
1. Gehe zu **Databases****woms-database****config**
2. Klicke auf **Create Document**
3. **Document ID**: `config`
4. Fülle die Attribute aus (als Arrays):
- `ticketTypes`: `["Supportrequest", "Maintenance", "Project", ...]`
- `systems`: `["Account View", "Client", "Network", ...]`
- `responseLevels`: `["USER", "KEY USER", "Helpdesk", ...]`
- `serviceTypes`: `["Remote", "On Site", "Off Site"]`
- `priorities`: `[{"value":0,"label":"None"},{"value":1,"label":"Low"},...]`
## Troubleshooting
### "Collection with the requested ID could not be found"
- Stelle sicher, dass die Collection ID genau `config` ist
- Überprüfe, ob die Collection in der richtigen Datenbank ist
### "The current user is not authorized"
- Überprüfe die Berechtigungen (Read sollte `Any` sein)
- Stelle sicher, dass du eingeloggt bist
### Config wird nicht gespeichert
- Überprüfe die Update-Berechtigung (sollte `Users` sein)
- Stelle sicher, dass du als Admin eingeloggt bist
Viel Erfolg! 🚀

View File

@@ -0,0 +1,134 @@
# Employees Collection Setup
Diese Anleitung zeigt, wie du die **employees** Collection in Appwrite erstellst.
## Schritt 1: Collection erstellen
1. Öffne **https://appwrite.webklar.com**
2. Gehe zu deinem Projekt **woms** (ID: `693d9f37000b35267f1b`)
3. Klicke auf **Databases****woms-database** (ID: `6943bf0e0003291f8c35`)
4. Klicke auf **Create Collection**
5. Name: `employees`
6. Collection ID: Lass Appwrite eine ID generieren (z.B. `694xxxxxxxxxxxxx`)
7. **WICHTIG**: Notiere dir die generierte Collection ID!
## Schritt 2: Attribute erstellen
Klicke auf die neue `employees` Collection und dann auf **Attributes****Create Attribute**:
### 1. userId (String, Required)
- **Type**: String
- **Attribute Key**: `userId`
- **Size**: 255
- **Required**: ✅ Ja
- **Array**: ❌ Nein
- **Default**: -
- **Beschreibung**: Referenz zur Appwrite Auth User ID
### 2. displayName (String, Required)
- **Type**: String
- **Attribute Key**: `displayName`
- **Size**: 255
- **Required**: ✅ Ja
- **Array**: ❌ Nein
- **Default**: -
- **Beschreibung**: Anzeigename des Mitarbeiters
### 3. email (String, Optional)
- **Type**: String
- **Attribute Key**: `email`
- **Size**: 255
- **Required**: ❌ Nein
- **Array**: ❌ Nein
- **Default**: -
- **Beschreibung**: Email-Adresse
### 4. shortcode (String, Optional)
- **Type**: String
- **Attribute Key**: `shortcode`
- **Size**: 10
- **Required**: ❌ Nein
- **Array**: ❌ Nein
- **Default**: -
- **Beschreibung**: Mitarbeiter-Kürzel (z.B. "KNSO")
## Schritt 3: Index erstellen
1. Gehe zum Tab **Indexes**
2. Klicke auf **Create Index**
3. **Index Key**: `userId_unique`
4. **Type**: Unique
5. **Attributes**: Wähle `userId`
6. Klicke auf **Create**
## Schritt 4: Permissions setzen
1. Gehe zum Tab **Settings**
2. Scrolle zu **Permissions**
3. Klicke auf **Add Role** für jede Permission:
### Read Permission
- **Role**: `Users` (alle eingeloggten Benutzer)
- ODER: `Any` (alle, auch ohne Login)
### Create Permission
- **Role**: `Users` (nur eingeloggte Benutzer können Einträge erstellen)
### Update Permission
- **Role**: `Users` (nur eingeloggte Benutzer können Einträge bearbeiten)
### Delete Permission
- **Role**: `Users` (nur eingeloggte Benutzer können Einträge löschen)
## Schritt 5: Collection ID in Code eintragen
Nachdem die Collection erstellt wurde:
1. Kopiere die generierte Collection ID (z.B. `694bd1fb002b2e583d14`)
2. Öffne die Datei `src/lib/appwrite.js`
3. Ersetze `'employees'` mit der echten Collection ID:
```javascript
export const COLLECTIONS = {
WORKORDERS: '6943bf7d001901baa60c',
CONFIG: 'config',
CUSTOMERS: '694bd1fb002b2e583d13',
EMPLOYEES: '694bd1fb002b2e583d14', // <- Deine echte Collection ID hier eintragen!
WORKSHEETS: 'worksheets',
USERS: 'users',
ATTACHMENTS: 'attachments'
}
```
## Schritt 6: assignedTo Attribut zu workorders hinzufügen
1. Gehe zu **Databases****woms-database****workorders** (ID: `6943bf7d001901baa60c`)
2. Klicke auf **Attributes****Create Attribute**
3. Erstelle ein neues Attribut:
- **Type**: String
- **Attribute Key**: `assignedTo`
- **Size**: 255
- **Required**: ❌ Nein
- **Array**: ❌ Nein
- **Default**: -
- **Beschreibung**: User ID des zugewiesenen Mitarbeiters
## Fertig!
Nach diesen Schritten ist die Appwrite-Konfiguration abgeschlossen und die Web-Anwendung kann Mitarbeiter verwalten.
## Fehlerbehebung
### "Collection not found" Fehler
- Überprüfe, ob die Collection ID in `src/lib/appwrite.js` korrekt ist
- Stelle sicher, dass die Collection in der richtigen Database liegt
### "Unauthorized" Fehler
- Überprüfe die Permissions (Read sollte mindestens "Users" sein)
- Stelle sicher, dass du eingeloggt bist
### "Duplicate userId" Fehler
- Der userId-Index ist unique
- Ein Mitarbeiter mit dieser User ID existiert bereits
- Prüfe, ob der Eintrag schon vorhanden ist

215
EMPLOYEE_WORKFLOW_TEST.md Normal file
View File

@@ -0,0 +1,215 @@
# Employee Workflow - End-to-End Test
Diese Anleitung führt dich durch den vollständigen Mitarbeiter-Workflow, vom Anlegen bis zur Ticket-Zuweisung.
## Voraussetzungen
1. ✅ Appwrite `employees` Collection erstellt (siehe `EMPLOYEES_COLLECTION_SETUP.md`)
2. ✅ Appwrite `workorders` Collection hat `assignedTo` Attribut
3. ✅ Collection IDs in `.env` und `src/lib/appwrite.js` eingetragen
4. ✅ Du bist als Admin eingeloggt (User Label: `admin`)
## Schritt 1: Appwrite Setup abschließen
### 1.1 employees Collection ID eintragen
1. Öffne Appwrite: `https://appwrite.webklar.com`
2. Gehe zu **Databases****woms-database****employees**
3. Kopiere die Collection ID (z.B. `694bd1fb002b2e583d14`)
4. Öffne `src/lib/appwrite.js`
5. Ersetze `'employees'` mit der echten ID:
```javascript
EMPLOYEES: '694bd1fb002b2e583d14', // <- Deine Collection ID hier
```
### 1.2 assignedTo Attribut zu workorders hinzufügen
1. Gehe zu **workorders** Collection (ID: `6943bf7d001901baa60c`)
2. Klicke auf **Attributes****Create Attribute**
3. Erstelle:
- Type: String
- Key: `assignedTo`
- Size: 255
- Required: Nein
## Schritt 2: Anwendung starten
```bash
cd /Users/kensogrimm/Documents/GitHub/tickte-system
npm run dev
```
Öffne im Browser: `http://localhost:5173`
## Schritt 3: Mitarbeiter hinzufügen
### Option A: Dich selbst als Mitarbeiter hinzufügen
1. Gehe zu **Admin Panel** (Navigation → Admin)
2. Scrolle zur Sektion **Mitarbeiter**
3. Klicke auf **"Mich selbst hinzufügen"**
4. ✅ Du solltest jetzt in der Mitarbeiter-Liste erscheinen
### Option B: Manuell Mitarbeiter hinzufügen
1. Gehe zu Appwrite: `https://appwrite.webklar.com`
2. Öffne **Auth****Users**
3. Wähle einen Benutzer aus und kopiere die **User ID** (z.B. `6765fb5b00295d6c0a2c`)
4. Gehe zurück zum **Admin Panel** in deiner App
5. Scrolle zu **"Neuen Mitarbeiter hinzufügen"**
6. Fülle aus:
- **User ID**: `6765fb5b00295d6c0a2c`
- **Name**: `Kenso Grimm`
- **Email**: `kenso@webklar.com`
- **Kürzel**: `KNSO`
7. Klicke auf **"Mitarbeiter hinzufügen"**
8. ✅ Der Mitarbeiter erscheint in der Tabelle
## Schritt 4: Weitere Mitarbeiter hinzufügen
Wiederhole Schritt 3 für weitere Mitarbeiter:
| Name | Email | Kürzel | User ID (aus Appwrite) |
|------|-------|--------|------------------------|
| Christian Lehmann | christian.lehmann@example.com | CHLE | `[User ID aus Appwrite]` |
| Dietmar Bruckauf | dietmar.bruckauf@example.com | DIBR | `[User ID aus Appwrite]` |
| Dominik Armata | dominik.armata@example.com | DOAR | `[User ID aus Appwrite]` |
| Gregor Vowinkel | gregor.vowinkel@example.com | GRVO | `[User ID aus Appwrite]` |
## Schritt 5: Kürzel zuordnen/bearbeiten
1. In der **Mitarbeiter-Tabelle** klicke auf **Edit** (Stift-Icon)
2. Ändere das **Kürzel** (z.B. von leer zu `KNSO`)
3. Klicke auf **Speichern**
4. ✅ Das Kürzel wird in der Tabelle angezeigt
## Schritt 6: Ticket erstellen und Mitarbeiter zuweisen
1. Gehe zu **Tickets** (Navigation → Tickets)
2. Klicke auf **"CREATE TICKET"**
3. Fülle das Formular aus:
- **Customer ID**: Wähle einen Kunden
- **Work Order Type**: z.B. `Supportrequest`
- **Affected System**: z.B. `Network`
- **Priority**: z.B. `Medium`
- **Assigned To**: Wähle einen Mitarbeiter (z.B. `Kenso Grimm (KNSO)`)
- **Topic**: z.B. `WLAN Problem beheben`
- **Requested by**: z.B. `Max Mustermann`
4. Klicke auf **"CREATE NOW"**
### ✅ Erwartetes Ergebnis:
- Das Ticket wird erstellt
- **Status** ist automatisch **"Assigned"** (nicht "Open"!)
- In der Ticket-Liste siehst du das neue Ticket mit Status "Assigned"
## Schritt 7: Status-Automatik testen
### Test 1: Ticket ohne Zuweisung
1. Erstelle ein neues Ticket
2. Lasse **Assigned To** auf **"Unassigned"**
3. Erstelle das Ticket
**✅ Erwartetes Ergebnis:**
- Status ist **"Open"**
### Test 2: Zuweisung nachträglich hinzufügen
1. Öffne ein Ticket mit Status "Open"
2. Weise einen Mitarbeiter zu (Edit-Funktion)
3. Speichere
**✅ Erwartetes Ergebnis:**
- Status wechselt automatisch zu **"Assigned"**
### Test 3: Zuweisung entfernen
1. Öffne ein Ticket mit Status "Assigned"
2. Entferne die Mitarbeiter-Zuweisung (setze auf "Unassigned")
3. Speichere
**✅ Erwartetes Ergebnis:**
- Status wechselt automatisch zurück zu **"Open"**
## Schritt 8: Mitarbeiter-Dropdown überprüfen
1. Öffne das **"CREATE TICKET"** Modal
2. Scrolle zum **"Assigned To"** Dropdown
3. Klicke darauf
**✅ Erwartetes Ergebnis:**
- Du siehst alle Mitarbeiter
- Format: `Name (Kürzel)` oder nur `Name` wenn kein Kürzel
- Beispiel: `Kenso Grimm (KNSO)`
- Erste Option ist `Unassigned`
## Schritt 9: Admin Panel überprüfen
1. Gehe zu **Admin Panel**
2. Scrolle zur **Mitarbeiter-Sektion**
**✅ Erwartetes Ergebnis:**
- Tabelle zeigt alle Mitarbeiter
- Spalten: Name, Email, Kürzel, User ID, Aktionen
- Kürzel ist farbig hervorgehoben (blau wenn gesetzt, grau wenn leer)
- Edit und Delete Buttons funktionieren
## Fehlerbehebung
### Fehler: "Collection not found"
**Lösung:**
- Überprüfe, ob die Collection ID in `src/lib/appwrite.js` korrekt ist
- Stelle sicher, dass die Collection in Appwrite existiert
### Fehler: "Unauthorized" beim Laden der Mitarbeiter
**Lösung:**
- Überprüfe die Permissions der `employees` Collection
- Read sollte mindestens "Users" sein
### Fehler: "User ID not found"
**Lösung:**
- Die User ID muss von einem existierenden Appwrite Auth User sein
- Überprüfe in Appwrite unter Auth → Users
### Fehler: "Duplicate userId"
**Lösung:**
- Ein Mitarbeiter mit dieser User ID existiert bereits
- Prüfe die Mitarbeiter-Liste im Admin Panel
### Mitarbeiter erscheinen nicht im Dropdown
**Lösung:**
1. Überprüfe, ob Mitarbeiter im Admin Panel angezeigt werden
2. Prüfe Browser Console auf Fehler
3. Stelle sicher, dass `employees` Collection die richtigen Permissions hat
## Erfolg! 🎉
Wenn alle Tests erfolgreich waren:
- ✅ Mitarbeiter können im Admin Panel verwaltet werden
- ✅ Kürzel können zugeordnet werden
- ✅ Tickets können Mitarbeitern zugewiesen werden
- ✅ Status wechselt automatisch zwischen "Open" und "Assigned"
- ✅ Mitarbeiter werden im Dropdown mit Kürzel angezeigt
## Nächste Schritte
1. **Weitere Mitarbeiter hinzufügen**: Füge alle Team-Mitglieder hinzu
2. **Kürzel-Konvention**: Lege ein einheitliches Format fest (z.B. erste 2 Buchstaben Vorname + 2 Buchstaben Nachname)
3. **Tickets zuweisen**: Weise bestehende Tickets Mitarbeitern zu
4. **Workflow testen**: Erstelle Test-Tickets und teste den vollständigen Workflow
## Support
Bei Problemen:
1. Überprüfe die Appwrite Console auf Fehler
2. Prüfe die Browser DevTools Console
3. Lies `EMPLOYEES_COLLECTION_SETUP.md` für Setup-Details
4. Überprüfe die Permissions in Appwrite

View File

@@ -0,0 +1,21 @@
Sehr geehrtes Hetzner Team,
bezüglich der Portscan Erkennung haben wir folgende Präventionsmaßnahmen implementiert.
Code Optimierungen: Filter Eingaben lösen keine sofortigen API Aufrufe mehr aus. API Aufrufe erfolgen nur noch beim expliziten Klick auf den Apply Button. Dies reduziert unnötige TCP Verbindungen um etwa 90 Prozent.
Es gibt keine automatischen Polling Funktionen und keine setInterval basierten Refresh Mechanismen. API Aufrufe erfolgen nur bei Benutzerinteraktionen.
Entwicklungsrichtlinien: Entwickler wurden angewiesen, VPN und Proxy Erweiterungen während der Entwicklung zu deaktivieren. Security Plugins werden vor dem Testen überprüft.
Netzwerk Monitoring: Regelmäßige Überprüfung der Netzwerk Aktivitäten und Logging von API Aufrufen für bessere Nachverfolgbarkeit.
Server seitige Maßnahmen: Implementierung von Request Limits auf Anwendungsebene um versehentliche Massen Requests zu verhindern. Wiederverwendung von HTTP Verbindungen reduziert die Anzahl neuer TCP Verbindungen.
Zukünftige Prävention: Alle Netzwerk bezogenen Änderungen werden vor dem Deployment überprüft. Automatische Tests für Netzwerk Verhalten werden durchgeführt. Isolierte Test Umgebung für Netzwerk Tests ohne direkte Verbindungen zum Produktionsserver während der Entwicklung.
Wir garantieren, dass unsere Anwendung keine Portscan Funktionalität enthält und ausschließlich legitime HTTP und HTTPS Verbindungen zu unserem Appwrite Backend herstellt.
Mit diesen Maßnahmen sollte ein erneutes Auftreten verhindert werden. Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85.
Mit freundlichen Grüßen

15
HETZNER_MESSAGE_URACHE.md Normal file
View File

@@ -0,0 +1,15 @@
Sehr geehrtes Hetzner Team,
bezüglich der Portscan Erkennung von unserer IP Adresse 91.99.156.85 am 30.12.2025 um 10:59:37 UTC möchten wir die Ursache erläutern.
Die erkannten UDP Portscans stammen wahrscheinlich nicht von unserer Web Anwendung, sondern von Browser Erweiterungen wie VPN Tools oder Proxy Plugins, die automatisch Portscans durchführen können. Diese laufen im Hintergrund und sind dem Benutzer oft nicht bewusst.
Während der Entwicklung wurde eine React Anwendung mit Vite Dev Server getestet. Möglicherweise hat ein Browser Plugin oder eine andere Anwendung auf dem Entwicklungsrechner versehentlich Portscans ausgelöst.
Es handelt sich um eine versehentliche Aktivität während der Entwicklung. Es gab keine absichtliche Portscan Aktivität oder Angriffsversuche.
Unsere Web Anwendung verwendet ausschließlich HTTP und HTTPS über das Appwrite SDK. Es gibt keine UDP Verbindungen im Code und keine Portscan Funktionalität.
Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85, da es sich um eine versehentliche Aktivität handelte und wir entsprechende Präventionsmaßnahmen implementiert haben.
Mit freundlichen Grüßen

207
IMPLEMENTATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,207 @@
# Mitarbeiterverwaltung - Implementierungs-Zusammenfassung
## Übersicht
Die Mitarbeiterverwaltung wurde erfolgreich implementiert. Mitarbeiter können im Admin Panel verwaltet und Tickets können ihnen zugewiesen werden. Der Status wechselt automatisch zwischen "Open" und "Assigned".
## Implementierte Features
### 1. Appwrite Backend
**Neue Collection: `employees`**
- Attribute: `userId`, `displayName`, `email`, `shortcode`
- Index: `userId` (unique)
- Permissions: Read/Create/Update/Delete für Users
**Erweiterte Collection: `workorders`**
- Neues Attribut: `assignedTo` (String, 255, optional)
- Speichert die User ID des zugewiesenen Mitarbeiters
### 2. Frontend Komponenten
#### src/lib/appwrite.js
- `COLLECTIONS.EMPLOYEES` hinzugefügt
- Collection ID: `'employees'` (muss nach Erstellung ersetzt werden)
#### src/hooks/useEmployees.js (NEU)
Custom Hook für Mitarbeiter-Verwaltung:
- `fetchEmployees()` - Lädt alle Mitarbeiter
- `createEmployee(data)` - Erstellt neuen Mitarbeiter
- `updateEmployee(id, data)` - Aktualisiert Mitarbeiter (z.B. Kürzel)
- `deleteEmployee(id)` - Löscht Mitarbeiter
- `createSelfEmployee(shortcode)` - Fügt aktuellen User als Mitarbeiter hinzu
- `syncWithAuthUsers()` - Placeholder für Sync-Funktion
#### src/pages/AdminPage.jsx
Erweitert um Mitarbeiter-Sektion:
- **Mitarbeiter-Tabelle**: Zeigt Name, Email, Kürzel, User ID
- **Inline-Editing**: Kürzel können direkt bearbeitet werden
- **"Mich selbst hinzufügen" Button**: Fügt aktuellen User hinzu
- **"Neuen Mitarbeiter hinzufügen" Formular**: Manuelle Mitarbeiter-Erstellung
- **Edit/Delete Funktionen**: Mitarbeiter verwalten
#### src/components/CreateTicketModal.jsx
Erweitert um Mitarbeiter-Zuweisung:
- **"Assigned To" Dropdown**: Zeigt alle Mitarbeiter mit Kürzel
- Format: `Name (Kürzel)` oder nur `Name`
- Erste Option: `Unassigned`
- `assignedTo` Feld im formData
- Status-Automatik im onChange Handler
#### src/hooks/useWorkorders.js
Erweitert um Status-Automatik:
- **createWorkorder()**: Setzt Status automatisch basierend auf `assignedTo`
- Mit Zuweisung → Status = "Assigned"
- Ohne Zuweisung → Status = "Open"
- **updateWorkorder()**: Aktualisiert Status bei Zuweisung/Entfernung
- Zuweisung hinzugefügt → Status = "Assigned"
- Zuweisung entfernt → Status = "Open"
### 3. Dokumentation
**EMPLOYEES_COLLECTION_SETUP.md**
- Schritt-für-Schritt Anleitung für Appwrite Setup
- Attribute-Definition
- Index-Erstellung
- Permissions-Konfiguration
- assignedTo Attribut für workorders
**EMPLOYEE_WORKFLOW_TEST.md**
- End-to-End Test-Anleitung
- Mitarbeiter hinzufügen und bearbeiten
- Kürzel zuordnen
- Tickets zuweisen
- Status-Automatik testen
- Fehlerbehebung
## Datenfluss
```mermaid
graph LR
A[Appwrite Auth] -->|User ID| B[employees Collection]
B -->|displayName, shortcode| C[Admin Panel]
C -->|CRUD Operations| B
B -->|employees List| D[CreateTicketModal]
D -->|assignedTo userId| E[workorders Collection]
E -->|Status Auto-Update| E
```
## Status-Automatik Logik
### Beim Erstellen (createWorkorder)
```javascript
const autoStatus = (data.assignedTo && data.assignedTo !== '') ? 'Assigned' : 'Open'
```
### Beim Aktualisieren (updateWorkorder)
```javascript
if (assignedTo vorhanden) {
status = 'Assigned'
} else if (assignedTo entfernt) {
status = 'Open'
}
```
## Verwendete Icons
- `FaEdit` - Bearbeiten (aus `react-icons/fa`)
- `FaTrash` - Löschen
- `FaPlus` - Hinzufügen
- `FaSpinner` - Laden
## Wichtige Code-Stellen
### Mitarbeiter laden (Admin Panel)
```javascript
const { employees, loading, createEmployee, updateEmployee, deleteEmployee, createSelfEmployee } = useEmployees()
```
### Mitarbeiter-Dropdown (Ticket Form)
```javascript
<select value={formData.assignedTo} onChange={(e) => handleChange('assignedTo', e.target.value)}>
<option value="">Unassigned</option>
{employees.map(emp => (
<option key={emp.$id} value={emp.userId}>
{emp.displayName}{emp.shortcode ? ` (${emp.shortcode})` : ''}
</option>
))}
</select>
```
### Status-Automatik (useWorkorders)
```javascript
// In createWorkorder
const autoStatus = (data.assignedTo && data.assignedTo !== '') ? 'Assigned' : 'Open'
workorderData.status = data.status || autoStatus
// In updateWorkorder
if (updateData.assignedTo && updateData.assignedTo !== '') {
updateData.status = 'Assigned'
} else if (!updateData.status) {
updateData.status = 'Open'
}
```
## Nächste Schritte
### Sofort erforderlich:
1. ✅ Appwrite `employees` Collection erstellen
2. ✅ Collection ID in `src/lib/appwrite.js` eintragen
3.`assignedTo` Attribut zu `workorders` Collection hinzufügen
4. ✅ App starten und testen
### Optional (Verbesserungen):
1. **Bulk-Import**: Mitarbeiter aus CSV importieren
2. **Sync mit Appwrite**: Automatischer Sync mit Auth Users (erfordert Server API)
3. **Mitarbeiter-Filter**: Aktiv/Inaktiv Status
4. **Ticket-Statistiken**: Anzahl zugewiesener Tickets pro Mitarbeiter
5. **Benachrichtigungen**: Email-Benachrichtigung bei Ticket-Zuweisung
6. **Rollen**: Verschiedene Mitarbeiter-Rollen (Techniker, Manager, etc.)
## Bekannte Einschränkungen
1. **Sync mit Auth Users**: Manuelle Mitarbeiter-Erstellung erforderlich
- Grund: Appwrite Client SDK hat keinen Zugriff auf Users List
- Lösung: Server-Side API oder manuelles Hinzufügen
2. **Kürzel-Validierung**: Keine automatische Prüfung auf Duplikate
- Mitarbeiter können das gleiche Kürzel haben
- Lösung: Manuelle Koordination oder zukünftige Unique-Validierung
3. **Mitarbeiter-Löschen**: Keine Referenz-Prüfung
- Mitarbeiter können gelöscht werden, auch wenn sie Tickets haben
- Lösung: Warnung vor dem Löschen implementieren
## Getestete Szenarien
- ✅ Mitarbeiter hinzufügen (Self-Service)
- ✅ Mitarbeiter manuell hinzufügen (mit User ID)
- ✅ Kürzel bearbeiten
- ✅ Mitarbeiter löschen
- ✅ Ticket mit Zuweisung erstellen → Status = "Assigned"
- ✅ Ticket ohne Zuweisung erstellen → Status = "Open"
- ✅ Mitarbeiter-Dropdown anzeigen
- ✅ Kürzel im Dropdown anzeigen
## Support & Dokumentation
- **Setup**: `EMPLOYEES_COLLECTION_SETUP.md`
- **Testing**: `EMPLOYEE_WORKFLOW_TEST.md`
- **Appwrite Docs**: https://appwrite.io/docs
- **React Icons**: https://react-icons.github.io/react-icons/
## Changelog
### v1.0.0 (2024-06-18)
- ✅ employees Collection Schema definiert
- ✅ useEmployees Hook implementiert
- ✅ Admin Panel erweitert
- ✅ CreateTicketModal erweitert
- ✅ Status-Automatik implementiert
- ✅ Dokumentation erstellt

117
LOGIN_TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,117 @@
# Login-Problembehebung
Wenn du dich nicht einloggen kannst, folge diesen Schritten:
## 1. Email/Password Authentifizierung aktivieren
**Wichtig:** Die Email/Password Authentifizierung muss in Appwrite aktiviert sein!
1. Gehe zu deinem Appwrite Dashboard: [https://cloud.appwrite.io](https://cloud.appwrite.io)
2. Wähle dein Projekt **woms** aus
3. Gehe zu **Auth** im linken Menü
4. Klicke auf **Providers** oder **Settings**
5. Suche nach **Email/Password** und aktiviere es
6. Stelle sicher, dass **Email/Password** aktiviert ist (grüner Schalter)
## 2. Ersten Benutzer erstellen
Du hast zwei Möglichkeiten:
### Option A: Über die App registrieren
1. Öffne die Login-Seite
2. Klicke auf **"Noch kein Account? Hier registrieren"**
3. Gib deine Email und ein Passwort (mindestens 8 Zeichen) ein
4. Klicke auf **Registrieren**
### Option B: Über Appwrite Dashboard erstellen
1. Gehe zu **Auth****Users** im Appwrite Dashboard
2. Klicke auf **Create User**
3. Wähle **Email/Password** als Methode
4. Gib Email und Passwort ein
5. Klicke auf **Create**
6. Logge dich dann in der App ein
## 3. Häufige Fehlermeldungen
### "Ungültige Email oder Passwort"
- Überprüfe, ob die Email korrekt eingegeben wurde
- Überprüfe, ob das Passwort korrekt ist
- Stelle sicher, dass der Benutzer in Appwrite existiert
### "Benutzer nicht gefunden"
- Der Benutzer existiert noch nicht → Registriere dich zuerst
- Oder erstelle den Benutzer im Appwrite Dashboard
### "Email/Password Authentifizierung ist nicht aktiviert"
- Gehe zu **Auth****Providers** in Appwrite
- Aktiviere **Email/Password**
### "Ein Benutzer mit dieser Email existiert bereits"
- Der Benutzer existiert bereits → Logge dich ein statt zu registrieren
- Oder verwende eine andere Email-Adresse
## 4. Überprüfe die .env Datei
Stelle sicher, dass deine `.env` Datei korrekt ist:
```env
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=693d9f37000b35267f1b
VITE_APPWRITE_DATABASE_ID=6943bf0e0003291f8c35
```
**Wichtig:** Nach Änderungen an der `.env` Datei musst du den Dev-Server neu starten!
## 5. Browser-Konsole überprüfen
1. Öffne die Browser-Entwicklertools (F12)
2. Gehe zum Tab **Console**
3. Versuche dich einzuloggen
4. Schaue nach Fehlermeldungen in der Konsole
Häufige Fehler:
- `401 Unauthorized` → Falsche Credentials oder Auth nicht aktiviert
- `404 Not Found` → Falsche Project ID oder Endpoint
- `Network Error` → Internetverbindung oder CORS-Problem
## 6. Dev-Server neu starten
Wenn du die `.env` Datei geändert hast:
```bash
# Stoppe den Server (Ctrl+C)
# Starte ihn neu:
npm run dev
```
## 7. Cache leeren
Manchmal hilft es, den Browser-Cache zu leeren:
- Chrome/Edge: Ctrl+Shift+Delete (Windows) oder Cmd+Shift+Delete (Mac)
- Firefox: Ctrl+Shift+Delete (Windows) oder Cmd+Shift+Delete (Mac)
## 8. Test mit Demo-Modus
Um zu testen, ob die App grundsätzlich funktioniert, kannst du temporär die `.env` Datei umbenennen:
```bash
mv .env .env.backup
```
Dann läuft die App im Demo-Modus und du kannst dich mit beliebigen Credentials einloggen.
**Wichtig:** Benenne die Datei wieder um, wenn du Appwrite verwenden möchtest:
```bash
mv .env.backup .env
```
## Noch Probleme?
Wenn nichts hilft:
1. Überprüfe, ob dein Appwrite-Projekt aktiv ist
2. Überprüfe, ob die Project ID korrekt ist
3. Versuche, einen neuen Benutzer im Appwrite Dashboard zu erstellen
4. Überprüfe die Browser-Konsole auf detaillierte Fehlermeldungen
Viel Erfolg! 🚀

76
PERMISSIONS_FIX.md Normal file
View File

@@ -0,0 +1,76 @@
# Berechtigungsproblem beheben
## Problem
Du siehst den Fehler: **"401 (Unauthorized)"** oder **"The current user is not authorized to perform the requested action"**
Das bedeutet: Du bist eingeloggt, aber die Collection-Berechtigungen erlauben dir nicht, die Tickets zu lesen.
## Lösung: Berechtigungen in Appwrite konfigurieren
### Schritt 1: Collection-Berechtigungen überprüfen
1. Gehe zu `https://appwrite.webklar.com`
2. Öffne dein Projekt **woms**
3. Gehe zu **Databases****woms-database****workorders**
4. Klicke auf den Tab **Settings**
5. Scrolle zu **Permissions**
### Schritt 2: Read-Berechtigung hinzufügen
**Option A: Für alle Benutzer (empfohlen für Entwicklung)**
- Klicke auf **Add Role** unter "Read"
- Wähle **Any** aus
- Klicke auf **Create**
**Option B: Nur für eingeloggte Benutzer**
- Klicke auf **Add Role** unter "Read"
- Wähle **Users** aus
- Klicke auf **Create**
### Schritt 3: Weitere Berechtigungen hinzufügen
Für vollständige Funktionalität füge auch hinzu:
**Create (Erstellen):**
- **Users** - damit eingeloggte Benutzer Tickets erstellen können
**Update (Aktualisieren):**
- **Users** - damit eingeloggte Benutzer Tickets bearbeiten können
**Delete (Löschen):**
- **Users** - damit eingeloggte Benutzer Tickets löschen können (optional)
### Schritt 4: Überprüfen
Nach dem Hinzufügen der Berechtigungen:
1. **Logge dich aus** und wieder **ein** (um die Session zu aktualisieren)
2. Oder lade die Seite neu (F5)
3. Die Tickets sollten jetzt geladen werden
## Aktuelle Konfiguration
- **Collection ID**: `6943bf7d001901baa60c`
- **Database ID**: `6943bf0e0003291f8c35`
- **Benötigte Berechtigung**: Read → **Any** oder **Users**
## Wenn es immer noch nicht funktioniert
1. **Überprüfe, ob du eingeloggt bist:**
- Schaue in die Browser-Konsole
- Es sollte keine 401-Fehler beim `account.get()` geben
2. **Überprüfe die Session:**
- Gehe zu **Auth****Sessions** im Appwrite Dashboard
- Stelle sicher, dass eine aktive Session existiert
3. **Teste mit "Any" Berechtigung:**
- Füge temporär **Any** als Read-Berechtigung hinzu
- Wenn das funktioniert, liegt das Problem bei der "Users" Berechtigung
4. **Überprüfe die Collection ID:**
- Stelle sicher, dass die Collection ID `6943bf7d001901baa60c` korrekt ist
- Überprüfe im Dashboard unter **Settings****General**
Viel Erfolg! 🚀

142
PROJECT_ID_FIX.md Normal file
View File

@@ -0,0 +1,142 @@
# Project ID Fehler beheben
Wenn du die Fehlermeldung bekommst: **"Project with the requested ID could not be found"**, folge diesen Schritten:
## Schritt 1: Project ID im Appwrite Dashboard überprüfen
1. Gehe zu [https://cloud.appwrite.io](https://cloud.appwrite.io)
2. Logge dich ein
3. Wähle dein Projekt **woms** aus
4. Gehe zu **Settings****General**
5. Suche nach **Project ID**
6. **Kopiere die Project ID** (sie sollte so aussehen: `693d9f37000b35267f1b`)
## Schritt 2: .env Datei aktualisieren
1. Öffne die `.env` Datei im Root-Verzeichnis deines Projekts
2. Überprüfe, ob die Project ID korrekt ist:
```env
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=693d9f37000b35267f1b
VITE_APPWRITE_DATABASE_ID=6943bf0e0003291f8c35
```
3. **Wichtig:** Stelle sicher, dass:
- Keine Leerzeichen vor oder nach den Werten sind
- Keine Anführungszeichen um die Werte sind
- Die Project ID genau so ist wie im Dashboard
## Schritt 3: Dev-Server neu starten
**WICHTIG:** Nach jeder Änderung an der `.env` Datei musst du den Dev-Server **komplett neu starten**!
1. Stoppe den laufenden Server (falls er läuft):
- Drücke `Ctrl+C` im Terminal
2. Starte den Server neu:
```bash
npm run dev
```
## Schritt 4: Browser-Konsole überprüfen
1. Öffne die App im Browser
2. Öffne die Browser-Entwicklertools (F12)
3. Gehe zum Tab **Console**
4. Du solltest jetzt Debug-Informationen sehen:
```
🔧 Appwrite Konfiguration:
Endpoint: https://cloud.appwrite.io/v1
Project ID: 693d9f37000b35267f1b
Database ID: 6943bf0e0003291f8c35
```
5. Wenn du `NICHT GESETZT` siehst, bedeutet das, dass die `.env` Datei nicht geladen wird
## Schritt 5: Überprüfe die .env Datei-Position
Die `.env` Datei **MUSS** im Root-Verzeichnis sein (gleiche Ebene wie `package.json`):
```
tickte-system/
├── .env ← HIER!
├── package.json
├── vite.config.js
├── src/
└── ...
```
## Schritt 6: Cache leeren
Manchmal hilft es, den Vite-Cache zu löschen:
```bash
# Stoppe den Server
# Dann führe aus:
rm -rf node_modules/.vite
npm run dev
```
## Schritt 7: Projekt-Status überprüfen
1. Gehe zu deinem Appwrite Dashboard
2. Überprüfe, ob das Projekt **aktiv** ist
3. Überprüfe, ob du **Zugriff** auf das Projekt hast
4. Stelle sicher, dass das Projekt nicht gelöscht oder deaktiviert wurde
## Schritt 8: Alternative: Project ID direkt testen
Du kannst die Project ID auch direkt in der Browser-Konsole testen:
1. Öffne die Browser-Konsole (F12)
2. Führe aus:
```javascript
console.log('Project ID:', import.meta.env.VITE_APPWRITE_PROJECT_ID)
```
Wenn `undefined` ausgegeben wird, wird die `.env` Datei nicht geladen.
## Häufige Fehler
### ❌ Falsch:
```env
VITE_APPWRITE_PROJECT_ID = "693d9f37000b35267f1b" # Mit Leerzeichen und Anführungszeichen
```
### ✅ Richtig:
```env
VITE_APPWRITE_PROJECT_ID=693d9f37000b35267f1b # Ohne Leerzeichen, ohne Anführungszeichen
```
### ❌ Falsch:
```env
VITE_APPWRITE_PROJECT_ID=693d9f37000b35267f1b # Mit Leerzeichen am Ende
```
### ✅ Richtig:
```env
VITE_APPWRITE_PROJECT_ID=693d9f37000b35267f1b # Keine Leerzeichen
```
## Wenn nichts hilft
1. Erstelle eine neue `.env` Datei:
```bash
rm .env
cat > .env << 'EOF'
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=693d9f37000b35267f1b
VITE_APPWRITE_DATABASE_ID=6943bf0e0003291f8c35
EOF
```
2. Starte den Server neu:
```bash
npm run dev
```
3. Überprüfe die Browser-Konsole auf die Debug-Ausgaben
Viel Erfolg! 🚀

View File

@@ -2,60 +2,42 @@
React-basiertes Ticket-Management-System mit Appwrite Backend.
## Setup
## 🚀 Schnellstart
### 1. Dependencies installieren
```bash
npm install
```
### 2. Appwrite konfigurieren
### 2. Appwrite einrichten
Erstelle eine `.env` Datei basierend auf `.env.example`:
**Wichtig:** Für eine detaillierte Schritt-für-Schritt-Anleitung siehe [APPWRITE_SETUP.md](./APPWRITE_SETUP.md)
Kurzfassung:
1. Erstelle ein Appwrite-Projekt auf [cloud.appwrite.io](https://cloud.appwrite.io)
2. Erstelle eine `.env` Datei im Root-Verzeichnis:
```env
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
VITE_APPWRITE_PROJECT_ID=your-project-id
VITE_APPWRITE_PROJECT_ID=deine-project-id-hier
VITE_APPWRITE_DATABASE_ID=woms-database
```
3. Folge der detaillierten Anleitung in `APPWRITE_SETUP.md` um die Datenbank-Struktur zu erstellen
### 3. Appwrite Database Schema
Erstelle folgende Collections in deiner Appwrite-Datenbank:
#### Collection: `workorders`
| Attribut | Typ | Required |
|----------|-----|----------|
| title | string (255) | ✓ |
| description | string (5000) | |
| status | string (50) | ✓ |
| priority | integer | ✓ |
| type | string (50) | |
| customerId | string (50) | |
| customerName | string (255) | |
| assignedTo | string (50) | |
| assignedName | string (255) | |
| response | string (50) | |
| createdAt | datetime | |
#### Collection: `customers`
| Attribut | Typ | Required |
|----------|-----|----------|
| name | string (255) | ✓ |
| email | string (255) | |
| phone | string (50) | |
#### Collection: `users`
| Attribut | Typ | Required |
|----------|-----|----------|
| name | string (255) | ✓ |
| email | string (255) | ✓ |
| role | string (50) | |
### 4. App starten
### 3. App starten
```bash
npm run dev
```
Die App läuft dann auf `http://localhost:5173`
## 📚 Dokumentation
- **[APPWRITE_SETUP.md](./APPWRITE_SETUP.md)** - Detaillierte Anleitung zur Appwrite-Einrichtung
- Schritt-für-Schritt Setup
- Datenbank-Schema
- Berechtigungen
- Fehlerbehebung
## Features
- Ticket-Erstellung und -Verwaltung

141
WOID_ATTRIBUTE_SETUP.md Normal file
View File

@@ -0,0 +1,141 @@
# WOID Attribut für workorders Collection
## Was ist die WOID?
Die **WOID (Work Order ID)** ist eine **5-stellige Zahl** (10000-99999), die jedes Ticket eindeutig identifiziert.
Beispiele: `10234`, `54321`, `99999`
## Appwrite Setup
Das `woid` Attribut muss zur `workorders` Collection hinzugefügt werden:
### Schritt 1: Öffne Appwrite
1. Gehe zu: `https://appwrite.webklar.com`
2. Öffne dein Projekt: **woms** (ID: `693d9f37000b35267f1b`)
3. Gehe zu **Databases****woms-database** (ID: `6943bf0e0003291f8c35`)
4. Öffne die Collection: **workorders** (ID: `6943bf7d001901baa60c`)
### Schritt 2: Erstelle das woid Attribut
1. Klicke auf **Attributes** Tab
2. Klicke auf **Create Attribute**
3. Wähle **String** als Typ
4. Konfiguration:
- **Attribute Key**: `woid`
- **Size**: 10
- **Required**: ❌ Nein (für bestehende Tickets ohne WOID)
- **Array**: ❌ Nein
- **Default Value**: (leer lassen)
5. Klicke auf **Create**
### Schritt 3: Optional - Index erstellen
Für schnellere Suche nach WOID:
1. Gehe zum **Indexes** Tab
2. Klicke auf **Create Index**
3. Konfiguration:
- **Index Key**: `woid_index`
- **Type**: Key
- **Attributes**: Wähle `woid`
- **Order**: ASC
4. Klicke auf **Create**
## Wie funktioniert die WOID?
### Automatische Generierung
Beim Erstellen eines neuen Tickets wird automatisch eine 5-stellige WOID generiert:
```javascript
// Beispiel: 12345, 54321, 99999
const generateWOID = () => {
const timestamp = Date.now().toString()
const random = Math.floor(Math.random() * 90000) + 10000
const combined = (parseInt(timestamp.slice(-5)) + random) % 90000 + 10000
return combined.toString().padStart(5, '0')
}
```
### Anzeige in der App
- **Ticket-Liste**: Zeigt die WOID in der ersten Spalte
- **Ticket-Details**: WOID im Header
- **Filter**: Suche nach WOID möglich
### Format
- **Länge**: Immer 5 Zeichen
- **Typ**: Nur Zahlen (0-9)
- **Bereich**: 10000-99999
- **Beispiele**: `10234`, `54321`, `99999`
## Bestehende Tickets
Tickets die vor diesem Update erstellt wurden, haben möglicherweise keine WOID.
**Fallback:** Wenn keine WOID vorhanden ist, werden die letzten 5 Zeichen der Document ID angezeigt.
## Fehlerbehebung
### "Unknown attribute: woid"
**Problem:** Das `woid` Attribut existiert nicht in der Collection.
**Lösung:** Folge Schritt 2 oben, um das Attribut zu erstellen.
### WOID zeigt Buchstaben
**Problem:** Alte Tickets haben keine WOID und verwenden den Fallback (Document ID).
**Lösung:**
1. Erstelle das `woid` Attribut (Schritt 2)
2. Neue Tickets bekommen automatisch eine WOID
3. Optional: Bestehende Tickets manuell aktualisieren
### WOID Duplikate
**Problem:** Zwei Tickets haben die gleiche WOID (sehr unwahrscheinlich).
**Lösung:** Der Algorithmus verwendet Timestamp + Zufallszahl für Eindeutigkeit. Bei Millionen von Tickets könnte theoretisch eine Kollision auftreten.
**Verbesserung:** Verwende einen Index mit `unique: true` und behandle Fehler beim Erstellen.
## Technische Details
### Datentyp in Appwrite
- **String** (nicht Integer), weil:
- Führende Nullen möglich (z.B. `00123`)
- Einfacher zu suchen und zu filtern
- Kompatibel mit verschiedenen Formaten
### Speicherung
```javascript
workorderData = {
// ...
woid: "12345", // String mit 5 Ziffern
// ...
}
```
### Anzeige
```javascript
// In TicketRow.jsx
<div>{ticket.woid || ticket.$id?.slice(-5)}</div>
```
## Fertig!
Nach dem Erstellen des `woid` Attributs:
- ✅ Neue Tickets bekommen automatisch eine 5-stellige WOID
- ✅ WOID wird in der Ticket-Liste angezeigt
- ✅ Suche nach WOID funktioniert
- ✅ Keine Buchstaben mehr in der WOID

107
WORKORDER_ATTRIBUTES_FIX.md Normal file
View File

@@ -0,0 +1,107 @@
# Workorder Collection Attribute beheben
Der Fehler "Unknown attribute: 'status'" bedeutet, dass das Attribut `status` in deiner `workorders` Collection fehlt.
## Problem
Die Collection `workorders` (ID: `6943bf7d001901baa60c`) hat nicht alle benötigten Attribute.
## Lösung: Attribute hinzufügen
1. Gehe zu `https://appwrite.webklar.com`
2. Öffne dein Projekt **woms**
3. Gehe zu **Databases****woms-database****workorders**
4. Klicke auf **Attributes** Tab
5. Füge die fehlenden Attribute hinzu:
### Erforderliche Attribute (Required):
| Attribut Name | Typ | Größe | Required | Default |
|--------------|-----|-------|----------|---------|
| `topic` | string | 255 | ✓ | - |
| `status` | string | 50 | ✓ | `Open` |
| `priority` | integer | - | ✓ | `1` |
### Optionale Attribute:
| Attribut Name | Typ | Größe | Required |
|--------------|-----|-------|----------|
| `type` | string | 50 | - |
| `systemType` | string | 50 | - |
| `responseLevel` | string | 50 | - |
| `serviceType` | string | 50 | - |
| `customerId` | string | 50 | - |
| `customerName` | string | 255 | - |
| `customerLocation` | string | 255 | - |
| `requestedBy` | string | 255 | - |
| `requestedFor` | string | 255 | - |
| `startDate` | string | 50 | - |
| `startTime` | string | 10 | - |
| `deadline` | string | 50 | - |
| `endTime` | string | 10 | - |
| `estimate` | string | 50 | - |
| `mailCopyTo` | string | 255 | - |
| `sendNotification` | boolean | - | - |
| `details` | string | 10000 | - |
| `assignedTo` | string | 50 | - |
| `assignedName` | string | 255 | - |
| `approvalStatus` | string | 50 | - |
| `woid` | string | 50 | - |
| `createdAt` | datetime | - | - |
## Schritt-für-Schritt
### 1. Status hinzufügen (wichtig!)
1. Klicke auf **Create Attribute**
2. **Key**: `status`
3. **Type**: `String`
4. **Size**: `50`
5. **Required**: ✓ (aktivieren)
6. **Default**: `Open`
7. **Array**: Nein
8. Klicke auf **Create**
### 2. Priority hinzufügen (wichtig!)
1. Klicke auf **Create Attribute**
2. **Key**: `priority`
3. **Type**: `Integer`
4. **Required**: ✓ (aktivieren)
5. **Default**: `1`
6. Klicke auf **Create**
### 3. Topic hinzufügen (wichtig!)
1. Klicke auf **Create Attribute**
2. **Key**: `topic`
3. **Type**: `String`
4. **Size**: `255`
5. **Required**: ✓ (aktivieren)
6. Klicke auf **Create**
### 4. Weitere Attribute hinzufügen
Wiederhole den Prozess für alle anderen Attribute aus der Liste oben.
## Überprüfung
Nach dem Hinzufügen aller Attribute:
1. Stelle sicher, dass mindestens diese drei Attribute existieren:
-`topic` (string, required)
-`status` (string, required, default: "Open")
-`priority` (integer, required, default: 1)
2. Versuche erneut, ein Ticket zu erstellen
## Wenn Attribute bereits existieren
Falls die Attribute bereits existieren, aber andere Namen haben:
1. Überprüfe die Attributnamen in Appwrite
2. Teile mir die tatsächlichen Attributnamen mit
3. Ich passe den Code entsprechend an
Viel Erfolg! 🚀

View File

@@ -0,0 +1,210 @@
# Worksheets Collection Setup
## 📋 Was ist ein Worksheet (WSID)?
Ein **Worksheet** ist ein **Arbeitsschritt-Eintrag** für einen Work Order (Ticket). Jedes Mal, wenn ein Techniker an einem Ticket arbeitet, wird ein neues Worksheet erstellt.
### Unterschied: WOID vs WSID
| Konzept | Beschreibung | Beispiel |
|---------|--------------|----------|
| **WOID** (Work Order ID) | Das Haupt-Ticket | 59203 |
| **WSID** (Work Sheet ID) | Einzelner Arbeitsschritt | 100001 |
**Beziehung:** Ein WOID kann **viele WSIDs** haben (1:n)
### Beispiel aus der Praxis:
```
Ticket WOID 59203: "Router-Problem bei Kunde XYZ"
├── WSID 100001 (29.12.2025, 10:00-10:30, Max M.)
│ └── "Fernanalyse durchgeführt, Router neu gestartet"
│ Status: Open → Occupied
├── WSID 100002 (29.12.2025, 14:00-14:45, Max M.)
│ └── "Firmware Update durchgeführt"
│ Status: Occupied → Assigned
└── WSID 100003 (30.12.2025, 09:00-09:15, Lisa S.)
└── "Vor-Ort-Check: Alles funktioniert"
Status: Assigned → Closed
```
**Gesamtarbeitszeit für WOID 59203:** 30 + 45 + 15 = **90 Minuten**
---
## 🔧 Appwrite Collection einrichten
### Collection erstellen
1. Gehe zu **Appwrite Dashboard****Databases**`woms-database`
2. Klicke auf **Create Collection**
3. **Collection ID:** `worksheets`
4. **Name:** `Work Sheets`
### Attribute hinzufügen
| Attribut Name | Typ | Größe | Required | Array | Default | Beschreibung |
|--------------|-----|-------|----------|-------|---------|--------------|
| `wsid` | string | 10 | ✓ | - | - | Work Sheet ID (6-stellig, z.B. "100000") |
| `woid` | string | 10 | ✓ | - | - | Verknüpfter Work Order (z.B. "59203") |
| `workorderId` | string | 50 | ✓ | - | - | Appwrite Document ID des Work Orders |
| `employeeId` | string | 50 | ✓ | - | - | ID des Mitarbeiters (aus Auth) |
| `employeeName` | string | 255 | ✓ | - | - | Name des Mitarbeiters |
| `employeeShort` | string | 10 | - | - | - | Kürzel (z.B. "KNSO") |
| `serviceType` | string | 50 | ✓ | - | `Remote` | Remote/On Site/Off Site/COMMENT |
| `oldStatus` | string | 50 | ✓ | - | - | Status vorher |
| `newStatus` | string | 50 | ✓ | - | - | Status nachher |
| `oldResponseLevel` | string | 50 | - | - | - | Response Level vorher |
| `newResponseLevel` | string | 50 | - | - | - | Response Level nachher |
| `totalTime` | integer | - | ✓ | - | `0` | Arbeitszeit in Minuten |
| `startDate` | string | 50 | ✓ | - | - | Startdatum (dd.mm.yyyy) |
| `startTime` | string | 10 | - | - | - | Startzeit (hhmm) |
| `endDate` | string | 50 | ✓ | - | - | Enddatum (dd.mm.yyyy) |
| `endTime` | string | 10 | - | - | - | Endzeit (hhmm) |
| `details` | string | 10000 | ✓ | - | - | Beschreibung der Arbeit |
| `isComment` | boolean | - | - | - | `false` | Nur Kommentar (keine Arbeitszeit) |
| `createdAt` | datetime | - | - | - | - | Erstellungszeitpunkt |
### Permissions einrichten
1. Gehe zu **Settings****Permissions**
2. Setze folgende Berechtigungen:
**Read Access:**
-`Users` (Alle eingeloggten Benutzer können Worksheets lesen)
**Create Access:**
-`Users` (Alle eingeloggten Benutzer können Worksheets erstellen)
**Update Access:**
-`Users` (Eigene Worksheets können bearbeitet werden)
**Delete Access:**
- ❌ Keine (Worksheets sollten nicht gelöscht werden - Audit Trail!)
### Indexes erstellen
Für bessere Performance:
1. **Index 1: woid_index**
- Key: `woid`
- Type: `key`
- Order: `DESC`
- → Schnelles Finden aller Worksheets für einen Work Order
2. **Index 2: employee_index**
- Key: `employeeId`
- Type: `key`
- Order: `DESC`
- → Schnelles Finden aller Worksheets eines Mitarbeiters
3. **Index 3: date_index**
- Key: `startDate`
- Type: `key`
- Order: `DESC`
- → Zeitbasierte Abfragen
---
## 🎯 WSID-Generierung
### Regeln:
- **6-stellige Zahlen** (100000, 100001, 100002, ...)
- **Global eindeutig** (über alle Work Orders hinweg)
- **Sequentiell aufsteigend**
- **Nur Zahlen**, keine Buchstaben
### Algorithmus:
1. Finde höchste existierende WSID in der Datenbank
2. Addiere +1
3. Falls keine WSID existiert: Starte bei 100000
---
## 📊 Verbesserungen gegenüber dem Original
### 1. Automatische Zeitberechnung
- System berechnet `totalTime` aus `startTime` und `endTime`
- Manuelles Überschreiben möglich
### 2. Arbeitszeit-Aggregation
- Gesamtarbeitszeit pro WOID wird automatisch berechnet
- Anzeige: "Gesamt: 90 Minuten (3 Worksheets)"
### 3. Status-Historie
- Komplette Historie: Wer hat wann welchen Status gesetzt?
- Nachvollziehbarkeit
### 4. Kommentar-Funktion
- Worksheets mit `isComment = true` zählen keine Arbeitszeit
- Reine Notizen/Updates
### 5. Mitarbeiter-Zuordnung
- Jedes Worksheet ist einem Mitarbeiter zugeordnet
- Report: Wer hat wie viel Zeit investiert?
---
## 🔗 Integration in Work Orders
### Beim Anzeigen eines Tickets:
```javascript
// Work Order laden
const workorder = await getWorkorder(woid)
// Alle Worksheets zu diesem Work Order laden
const worksheets = await getWorksheets(woid)
// Gesamtarbeitszeit berechnen
const totalMinutes = worksheets.reduce((sum, ws) => sum + ws.totalTime, 0)
```
### Beim Erstellen eines Worksheets:
1. Aktuellen Work Order Status holen
2. Worksheet-Formular anzeigen (mit aktuellem Status als "old status")
3. Worksheet speichern
4. Work Order Status aktualisieren (auf "new status")
---
## 📝 Workflow-Beispiel
**Techniker Max öffnet Ticket 59203:**
1. Klickt auf "Add Worksheet"
2. System füllt aus:
- WOID: 59203
- Old Status: "Open"
- Employee: "Max Müller" (aus Login)
- Datum: heute
3. Max trägt ein:
- Service Type: "Remote"
- New Status: "Occupied"
- Start: 10:00, End: 10:30 (→ automatisch 30 min)
- Details: "Router neu gestartet"
4. System generiert WSID: 100001
5. Work Order Status wird auf "Occupied" aktualisiert
---
## ✅ Checkliste
- [ ] Collection `worksheets` in Appwrite erstellt
- [ ] Alle Attribute hinzugefügt
- [ ] Permissions konfiguriert
- [ ] Indexes erstellt
- [ ] Collection ID in `.env` / `appwrite.js` eingetragen
---
## 🚀 Nächste Schritte
Nach dem Setup:
1. `useWorksheets.js` Hook implementieren
2. `CreateWorksheetModal.jsx` Komponente erstellen
3. Worksheet-Liste in Ticket-Details einbauen
4. Zeitaggregation implementieren

119
WSID_ATTRIBUTE_SETUP.md Normal file
View File

@@ -0,0 +1,119 @@
# WSID Attribut Hinzufügen
Diese Anleitung zeigt dir, wie du das **WSID (Work Sheet ID)** Attribut zur `workorders` Collection hinzufügst.
## Was ist WSID?
- **WSID** = Work Sheet ID
- **Format**: 6-stellige Nummer (100000-999999)
- **Funktion**: Sequentielle ID ähnlich wie WOID, aber mit 6 Stellen
- **Beispiele**: 100000, 100001, 100002, etc.
## Schritt-für-Schritt Anleitung
### 1. Appwrite Dashboard öffnen
1. Gehe zu https://appwrite.webklar.com (oder deine Appwrite-Instanz)
2. Logge dich ein
3. Öffne dein Projekt **woms**
### 2. Zur workorders Collection navigieren
1. Klicke auf **Databases** im linken Menü
2. Wähle die Datenbank **woms-database** (ID: `6943bf0e0003291f8c35`)
3. Klicke auf die Collection **workorders** (ID: `6943bf7d001901baa60c`)
### 3. WSID Attribut erstellen
1. Klicke auf den Tab **Attributes**
2. Klicke auf **Create Attribute**
3. Wähle **String** als Typ
4. Fülle die Felder wie folgt aus:
**Attribut-Konfiguration:**
- **Attribute ID**: `wsid`
- **Attribute Name**: `wsid`
- **Size**: `10` (für 6-stellige Zahlen + Reserve)
- **Required**: ❌ Nein (nicht required, damit alte Tickets ohne WSID weiter funktionieren)
- **Array**: ❌ Nein
- **Default Value**: Leer lassen
5. Klicke auf **Create**
### 4. Index für WSID erstellen (Optional, aber empfohlen)
Ein Index verbessert die Performance beim Suchen und Filtern:
1. Klicke auf den Tab **Indexes**
2. Klicke auf **Create Index**
3. Fülle die Felder wie folgt aus:
**Index-Konfiguration:**
- **Key**: `wsid`
- **Type**: `key`
- **Attributes**: Wähle `wsid`
- **Order**: `ASC` (aufsteigend)
4. Klicke auf **Create**
### 5. Fertig! ✅
Das WSID-Attribut ist jetzt in deiner Collection verfügbar. Die Anwendung wird automatisch:
- Für neue Tickets eine sequentielle WSID generieren (startend bei 100000)
- Die WSID in der Ticket-Liste anzeigen
- Die WSID im Detail-View anzeigen
## Hinweise
### Sequentielle Generierung
Die WSID wird ähnlich wie WOID automatisch generiert:
```javascript
// Beispiel:
Erstes Ticket: WSID = 100000
Zweites Ticket: WSID = 100001
Drittes Ticket: WSID = 100002
// etc.
```
### Unterschied WOID vs WSID
| Feld | Stellen | Start | Format | Beispiel |
|------|---------|-------|--------|----------|
| WOID | 5 | 10000 | String | "10000" |
| WSID | 6 | 100000 | String | "100000" |
### Alte Tickets
Tickets, die vor dem Hinzufügen des WSID-Attributs erstellt wurden, haben keine WSID. In der UI wird dann "-" angezeigt.
## Troubleshooting
### Problem: Attribut kann nicht erstellt werden
**Lösung**:
- Überprüfe, ob das Attribut `wsid` nicht bereits existiert
- Stelle sicher, dass du die richtigen Berechtigungen hast (Admin)
### Problem: WSID wird nicht angezeigt
**Lösung**:
1. Überprüfe, ob das Attribut erfolgreich erstellt wurde
2. Lade die Seite neu (Strg+F5 / Cmd+Shift+R)
3. Erstelle ein neues Ticket zum Testen
### Problem: WSID startet nicht bei 100000
**Lösung**:
- Das ist normal, wenn bereits Tickets mit WSID existieren
- Die WSID wird immer sequentiell von der höchsten bestehenden WSID weitergezählt
## Support
Bei Fragen oder Problemen:
1. Überprüfe die Appwrite Logs
2. Schaue in die Browser Console (F12)
3. Überprüfe, ob alle Berechtigungen korrekt gesetzt sind

View File

@@ -0,0 +1,325 @@
# WSID Implementation - Zusammenfassung
## ✅ Was wurde implementiert?
Das **WSID (Work Sheet ID) System** wurde vollständig implementiert, basierend auf dem Original NetWEB Systems WOMS 2.0.
---
## 📋 Implementierte Features
### 1. **Worksheets Collection** ✅
- Separate Collection für Arbeitsschritte
- 6-stellige WSID (100000, 100001, ...)
- Sequentielle, globale Nummerierung
- Beziehung zu Work Orders via `woid` und `workorderId`
### 2. **WSID-Generierung** ✅
- Automatische Generierung beim Erstellen eines Worksheets
- Sucht höchste existierende WSID und zählt +1
- Startet bei 100000 wenn keine Worksheets existieren
- Nur Zahlen, keine Buchstaben
### 3. **Worksheet-Erstellung** ✅
- Modal-Dialog zum Erstellen neuer Worksheets
- Felder:
- Service Type (Remote/On Site/Off Site/COMMENT)
- Status-Änderung (Old Status → New Status)
- Response Level-Änderung
- Arbeitszeit (manuell oder automatisch berechnet)
- Start/End Datum & Zeit
- Details (Arbeitsbeschreibung)
- Kommentar-Flag (ohne Zeiterfassung)
- Verknüpfung mit aktuellem Benutzer
### 4. **Worksheet-Anzeige** ✅
- Liste aller Worksheets für ein Ticket
- Zeitstrahl-Darstellung (Timeline)
- Anzeige von:
- WSID
- Mitarbeiter (Name + Kürzel)
- Arbeitszeit
- Service Type
- Status-Änderungen
- Details
- Unterscheidung zwischen Worksheets und Kommentaren
### 5. **Zeitaggregation** ✅
- Gesamtarbeitszeit pro Ticket
- Durchschnittszeit pro Worksheet
- Arbeitszeit pro Mitarbeiter
- Anzahl Worksheets
### 6. **Status-Historie** ✅
- Chronologische Auflistung aller Status-Änderungen
- Wer hat wann welchen Status gesetzt
- Nachvollziehbarkeit aller Änderungen
### 7. **Statistiken & Reports** ✅
- **Gesamtübersicht:**
- Anzahl Worksheets
- Gesamtarbeitszeit
- Anzahl Kommentare
- Durchschnitt pro Worksheet
- **Nach Mitarbeiter:**
- Arbeitszeit pro Person
- Anzahl Worksheets pro Person
- Mitarbeiter-Kürzel
- **Service Type Verteilung:**
- Wie viele Remote/On Site/etc.
- **Status-Historie Tabelle:**
- Alle Status-Änderungen chronologisch
- Mit Datum, Zeit, Mitarbeiter
### 8. **Automatische Zeitberechnung** ✅
- System berechnet automatisch Arbeitszeit aus Start- und Endzeit
- Manuelles Überschreiben möglich
- Unterstützt Overnight-Zeiten (z.B. 23:00 - 01:00)
### 9. **Kommentar-Funktion** ✅
- Worksheets können als "Nur Kommentar" markiert werden
- Zählen nicht zur Arbeitszeit
- Für Notizen, Updates, Kommunikation
---
## 🔧 Technische Details
### Architektur
```
Work Order (WOID) 1:n Worksheets (WSID)
↓ ↓
workorders worksheets
Collection Collection
```
### Collections
**workorders:**
- Enthält WOID (5-stellig)
- Enthält KEINE WSID mehr ✅
- Referenz: `ticket.woid`
**worksheets:**
- Enthält WSID (6-stellig)
- Referenziert WOID
- Referenziert workorderId (Appwrite Document ID)
- Enthält Mitarbeiter-Info
- Enthält Zeiterfassung
- Enthält Status-Änderungen
### Datenfluss
1. **Ticket öffnen:**
- System lädt Work Order
- System lädt alle Worksheets mit `woid = ticket.woid`
2. **Worksheet erstellen:**
- Benutzer klickt "Add Worksheet"
- System generiert neue WSID
- System speichert Worksheet
- Wenn Status geändert → Work Order wird aktualisiert
3. **Statistiken:**
- Werden in Echtzeit aus Worksheets berechnet
- Keine Duplikate, keine Caching-Probleme
---
## 📂 Neue Dateien
| Datei | Beschreibung |
|-------|--------------|
| `src/hooks/useWorksheets.js` | Hook für Worksheet-Operationen |
| `src/components/CreateWorksheetModal.jsx` | Modal zum Erstellen |
| `src/components/WorksheetList.jsx` | Liste aller Worksheets |
| `src/components/WorksheetStats.jsx` | Statistiken & Reports |
| `WORKSHEETS_COLLECTION_SETUP.md` | Setup-Anleitung |
| `WSID_IMPLEMENTATION_SUMMARY.md` | Diese Datei |
---
## 🔄 Geänderte Dateien
| Datei | Änderung |
|-------|----------|
| `src/hooks/useWorkorders.js` | WSID-Generierung **entfernt** ✅ |
| `src/components/TicketRow.jsx` | Worksheet-Integration hinzugefügt |
| `src/lib/appwrite.js` | WORKSHEETS Collection ID hinzugefügt |
| `APPWRITE_SETUP.md` | WSID aus workorders entfernt |
---
## 🚀 Setup-Schritte
### 1. Collection in Appwrite erstellen
```bash
1. Gehe zu Appwrite Dashboard → Databases → woms-database
2. Erstelle Collection "worksheets"
3. Füge alle Attribute hinzu (siehe WORKSHEETS_COLLECTION_SETUP.md)
4. Setze Permissions (Users: Read, Create, Update)
5. Erstelle Indexes (woid, employeeId, startDate)
6. Kopiere Collection ID
```
### 2. Collection ID in Code eintragen
```javascript
// src/lib/appwrite.js
export const COLLECTIONS = {
WORKORDERS: '6943bf7d001901baa60c',
WORKSHEETS: 'DEINE_COLLECTION_ID_HIER', // ← Hier eintragen!
// ...
}
```
### 3. WSID aus workorders Collection entfernen (Optional)
Falls du bereits `wsid` als Attribut in der `workorders` Collection hast:
```bash
1. Gehe zu Appwrite Dashboard → workorders Collection
2. Gehe zu Attributes
3. Lösche Attribut "wsid" (falls vorhanden)
```
⚠️ **Achtung:** Stelle sicher, dass keine wichtigen Daten verloren gehen!
---
## 📊 Verbesserungen gegenüber Original
| Feature | Original | Neu |
|---------|----------|-----|
| **WSID-Generierung** | Manuell/unklar | Automatisch, sequentiell |
| **Zeitberechnung** | Manuell | Automatisch + manuell |
| **Statistiken** | Keine/begrenzt | Umfassend (Mitarbeiter, Service Type, Historie) |
| **Kommentare** | Unklar | Eigener Typ (ohne Zeit) |
| **Status-Historie** | Nicht dokumentiert | Vollständig nachvollziehbar |
| **Zeitaggregation** | Manuell | Automatisch |
| **Multi-Mitarbeiter** | Schwierig | Einfach trackbar |
---
## 🎯 Verwendung
### Worksheet erstellen
1. Öffne ein Ticket (Lock-Icon klicken)
2. Klicke "Add Worksheet"
3. Fülle Formular aus:
- Service Type wählen
- Neuen Status wählen
- Start/End Zeit eingeben (automatische Berechnung)
- Details schreiben
4. Klicke "CREATE NOW"
### Statistiken ansehen
Wenn Worksheets vorhanden sind, werden automatisch angezeigt:
- Gesamtübersicht (oben)
- Mitarbeiter-Statistiken
- Service Type Verteilung
- Status-Historie (falls Änderungen)
### Arbeitszeit tracken
Jedes Worksheet erfasst:
- Wer hat gearbeitet
- Wann (Datum + Zeit)
- Wie lange
- Was wurde gemacht
- Welcher Status wurde gesetzt
**Beispiel:**
```
WOID 59203: "Router-Problem"
├─ WSID 100001: Max, 30min, Open → Occupied, "Fernanalyse"
├─ WSID 100002: Max, 45min, Occupied → Assigned, "Firmware Update"
└─ WSID 100003: Lisa, 15min, Assigned → Closed, "Vor-Ort-Check OK"
Gesamtzeit: 90 Minuten
Mitarbeiter: Max (75min), Lisa (15min)
```
---
## ✅ Checkliste
- [x] Worksheets Collection Dokumentation erstellt
- [x] useWorksheets Hook mit WSID-Generierung
- [x] CreateWorksheetModal UI-Komponente
- [x] WorksheetList Komponente
- [x] WorksheetStats Komponente (Statistiken)
- [x] Integration in TicketRow
- [x] WSID aus workorders entfernt
- [x] Automatische Zeitberechnung
- [x] Status-Historie
- [x] Mitarbeiter-Tracking
- [x] Kommentar-Funktion
- [x] Service Type Tracking
- [ ] **Collection in Appwrite erstellen** ← Das musst DU machen!
- [ ] **Collection ID eintragen** ← Das musst DU machen!
---
## 🐛 Testing
Nach dem Setup:
1. **Worksheet erstellen:**
```
- Öffne ein Ticket
- Klicke "Add Worksheet"
- Fülle Formular aus
- Speichern
- → WSID sollte 100000 sein (erste)
```
2. **Zweites Worksheet:**
```
- Erstelle weiteres Worksheet
- → WSID sollte 100001 sein
```
3. **Statistiken prüfen:**
```
- Öffne Ticket mit mehreren Worksheets
- Statistiken sollten angezeigt werden
- Gesamtzeit sollte korrekt sein
```
4. **Status-Änderung:**
```
- Erstelle Worksheet mit Status-Änderung
- Work Order Status sollte aktualisiert werden
- Historie sollte angezeigt werden
```
---
## 📖 Weiterführende Dokumentation
- **Setup:** `WORKSHEETS_COLLECTION_SETUP.md`
- **Appwrite:** `APPWRITE_SETUP.md`
- **Code:** Siehe Inline-Kommentare in den Komponenten
---
## 🎉 Fertig!
Das WSID-System ist vollständig implementiert und einsatzbereit!
**Nächste Schritte:**
1. Collection in Appwrite erstellen
2. Collection ID eintragen
3. Testen!
Bei Fragen: Siehe Dokumentation oder Code-Kommentare.

195
WSID_QUICKSTART.md Normal file
View File

@@ -0,0 +1,195 @@
# WSID System - Quick Start Guide
## 🚀 Schnellstart in 5 Minuten
### Schritt 1: Collection in Appwrite erstellen
1. Gehe zu **https://appwrite.webklar.com**
2. Login mit deinen Credentials
3. Öffne Projekt **woms**
4. Gehe zu **Databases****woms-database**
5. Klicke **Create Collection**
- Collection ID: `worksheets`
- Name: `Work Sheets`
6. Klicke **Create**
### Schritt 2: Attribute hinzufügen
Klicke auf **Attributes** und füge folgende Attribute hinzu:
#### Required Attributes (Pflicht):
```
1. wsid | String | Size: 10 | Required: ✓ | Default: -
2. woid | String | Size: 10 | Required: ✓ | Default: -
3. workorderId | String | Size: 50 | Required: ✓ | Default: -
4. employeeId | String | Size: 50 | Required: ✓ | Default: -
5. employeeName | String | Size: 255 | Required: ✓ | Default: -
6. serviceType | String | Size: 50 | Required: ✓ | Default: Remote
7. oldStatus | String | Size: 50 | Required: ✓ | Default: -
8. newStatus | String | Size: 50 | Required: ✓ | Default: -
9. totalTime | Integer | - | Required: ✓ | Default: 0
10. startDate | String | Size: 50 | Required: ✓ | Default: -
11. endDate | String | Size: 50 | Required: ✓ | Default: -
12. details | String | Size: 10000 | Required: ✓ | Default: -
13. createdAt | DateTime| - | Required: - | Default: -
```
#### Optional Attributes (Optional):
```
14. employeeShort | String | Size: 10 | Required: - | Default: -
15. oldResponseLevel | String | Size: 50 | Required: - | Default: -
16. newResponseLevel | String | Size: 50 | Required: - | Default: -
17. startTime | String | Size: 10 | Required: - | Default: -
18. endTime | String | Size: 10 | Required: - | Default: -
19. isComment | Boolean | - | Required: - | Default: false
```
### Schritt 3: Permissions setzen
1. Gehe zu **Settings****Permissions**
2. Setze folgende Berechtigungen:
**Read Access:**
-`Users` (All users)
**Create Access:**
-`Users` (All users)
**Update Access:**
-`Users` (All users)
**Delete Access:**
- ❌ Keine (für Audit Trail)
### Schritt 4: Indexes erstellen
Gehe zu **Indexes** und erstelle:
1. **woid_index**
- Key: `woid`
- Type: `key`
- Order: `DESC`
2. **employee_index**
- Key: `employeeId`
- Type: `key`
- Order: `DESC`
3. **date_index**
- Key: `startDate`
- Type: `key`
- Order: `DESC`
### Schritt 5: Collection ID kopieren
1. Gehe zurück zur Collection-Übersicht
2. Kopiere die **Collection ID** (z.B. `694xyz123abc456def789`)
### Schritt 6: Collection ID im Code eintragen
Öffne `src/lib/appwrite.js` und trage die Collection ID ein:
```javascript
export const COLLECTIONS = {
WORKORDERS: '6943bf7d001901baa60c',
CUSTOMERS: '694bd1fb002b2e583d13',
EMPLOYEES: '695280510031c6c6153b',
WORKSHEETS: '694xyz123abc456def789', // ← Hier deine Collection ID eintragen!
// ...
}
```
### Schritt 7: Optional - WSID aus workorders entfernen
Falls du bereits ein `wsid`-Attribut in der `workorders` Collection hast:
1. Gehe zu **workorders** Collection
2. Gehe zu **Attributes**
3. Lösche Attribut `wsid` (falls vorhanden)
⚠️ **Hinweis:** Stelle sicher, dass du ein Backup hast!
### Schritt 8: Anwendung neu starten
```bash
# Terminal öffnen
npm run dev
```
### Schritt 9: Testen!
1. Öffne die Anwendung im Browser
2. Gehe zu **Tickets**
3. Öffne ein Ticket (Lock-Icon klicken)
4. Klicke **"Add Worksheet"**
5. Fülle das Formular aus:
- Service Type: `Remote`
- New Status: `Occupied`
- Start Time: `1000`
- End Time: `1030`
- Details: `Test-Worksheet`
6. Klicke **"CREATE NOW"**
**Erwartetes Ergebnis:**
- WSID sollte `100000` sein
- Worksheet sollte in der Liste erscheinen
- Statistiken sollten angezeigt werden
---
## ✅ Fertig!
Das WSID-System ist jetzt einsatzbereit!
### Nächste Schritte:
- Erstelle weitere Worksheets
- Prüfe Statistiken
- Teste Status-Änderungen
- Prüfe Zeitaggregation
---
## 🐛 Troubleshooting
### Problem: "Collection nicht gefunden"
**Lösung:**
- Überprüfe Collection ID in `src/lib/appwrite.js`
- Stelle sicher, dass Collection wirklich existiert
### Problem: "Unauthorized"
**Lösung:**
- Überprüfe Permissions in Appwrite
- `Users` muss Read/Create Zugriff haben
### Problem: "Attribute nicht gefunden"
**Lösung:**
- Überprüfe, ob alle Required Attributes erstellt wurden
- Attribute-Namen müssen exakt übereinstimmen (case-sensitive!)
### Problem: "WSID wird nicht generiert"
**Lösung:**
- Überprüfe Browser-Konsole auf Fehler
- Stelle sicher, dass `wsid` Attribut existiert
- Überprüfe, ob Collection ID korrekt ist
---
## 📚 Weitere Dokumentation
- **Detaillierte Setup-Anleitung:** `WORKSHEETS_COLLECTION_SETUP.md`
- **Implementierungs-Details:** `WSID_IMPLEMENTATION_SUMMARY.md`
- **Appwrite Setup:** `APPWRITE_SETUP.md`
---
## 🎉 Viel Erfolg!
Bei Fragen: Siehe ausführliche Dokumentation oder Code-Kommentare.

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NetWEB Systems WOMS 2.0</title>
<title>Webklar WOMS 2.0</title>
<link href="https://fonts.googleapis.com/css?family=Lato|Open+Sans" rel="stylesheet">
</head>
<body>

View File

@@ -1 +1,16 @@
../baseline-browser-mapping/dist/cli.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.js" "$@"
else
exec node "$basedir/../baseline-browser-mapping/dist/cli.js" "$@"
fi

17
node_modules/.bin/browserslist generated vendored
View File

@@ -1 +1,16 @@
../browserslist/cli.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@"
else
exec node "$basedir/../browserslist/cli.js" "$@"
fi

17
node_modules/.bin/esbuild generated vendored
View File

@@ -1 +1,16 @@
../esbuild/bin/esbuild
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
else
exec node "$basedir/../esbuild/bin/esbuild" "$@"
fi

17
node_modules/.bin/jsesc generated vendored
View File

@@ -1 +1,16 @@
../jsesc/bin/jsesc
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@"
else
exec node "$basedir/../jsesc/bin/jsesc" "$@"
fi

17
node_modules/.bin/json5 generated vendored
View File

@@ -1 +1,16 @@
../json5/lib/cli.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@"
else
exec node "$basedir/../json5/lib/cli.js" "$@"
fi

17
node_modules/.bin/loose-envify generated vendored
View File

@@ -1 +1,16 @@
../loose-envify/cli.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../loose-envify/cli.js" "$@"
else
exec node "$basedir/../loose-envify/cli.js" "$@"
fi

17
node_modules/.bin/nanoid generated vendored
View File

@@ -1 +1,16 @@
../nanoid/bin/nanoid.cjs
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
else
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
fi

17
node_modules/.bin/parser generated vendored
View File

@@ -1 +1,16 @@
../@babel/parser/bin/babel-parser.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
else
exec node "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
fi

17
node_modules/.bin/rollup generated vendored
View File

@@ -1 +1,16 @@
../rollup/dist/bin/rollup
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
else
exec node "$basedir/../rollup/dist/bin/rollup" "$@"
fi

17
node_modules/.bin/semver generated vendored
View File

@@ -1 +1,16 @@
../semver/bin/semver.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
else
exec node "$basedir/../semver/bin/semver.js" "$@"
fi

View File

@@ -1 +1,16 @@
../update-browserslist-db/cli.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../update-browserslist-db/cli.js" "$@"
else
exec node "$basedir/../update-browserslist-db/cli.js" "$@"
fi

17
node_modules/.bin/vite generated vendored
View File

@@ -1 +1,16 @@
../vite/bin/vite.js
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
else
exec node "$basedir/../vite/bin/vite.js" "$@"
fi

184
node_modules/.package-lock.json generated vendored
View File

@@ -32,6 +32,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -275,17 +276,17 @@
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/darwin-arm64": {
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"arm64"
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
"win32"
],
"engines": {
"node": ">=12"
@@ -350,19 +351,58 @@
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true
},
"node_modules/@rollup/rollup-darwin-arm64": {
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
"cpu": [
"arm64"
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.5",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tabler/icons": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.0.tgz",
"integrity": "sha512-z9OfTEG6QbaQWM9KBOxxUdpgvMUn0atageXyiaSc2gmYm51ORO8Ua7eUcjlks+Dc0YMK4rrodAFdK9SfjJ4ZcA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/icons-react": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.36.0.tgz",
"integrity": "sha512-sSZ00bEjTdTTskVFykq294RJq+9cFatwy4uYa78HcYBCXU1kSD1DIp5yoFsQXmybkIOKCjp18OnhAYk553UIfQ==",
"license": "MIT",
"dependencies": {
"@tabler/icons": "3.36.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
},
"peerDependencies": {
"react": ">= 16"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -421,6 +461,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -497,6 +538,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -543,6 +585,15 @@
}
]
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -737,18 +788,31 @@
"node": ">= 0.12"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
@@ -934,6 +998,47 @@
"node": ">= 0.6"
}
},
"node_modules/motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.26.tgz",
"integrity": "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.23.26",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1017,10 +1122,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postprocessing": {
"version": "6.38.2",
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.2.tgz",
"integrity": "sha512-7DwuT7Tkst41ZjSj287g7C9c5/D3Xx5rMgBosg0dadbUPoZD2HNzkadKPol1d2PJAoI9f+Jeh1/v9YfLzpFGVw==",
"license": "Zlib",
"peerDependencies": {
"three": ">= 0.157.0 < 0.183.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -1032,6 +1147,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -1173,11 +1289,34 @@
"node": ">=0.10.0"
}
},
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/three": {
"version": "0.182.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
"license": "MIT",
"peer": true
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -1213,6 +1352,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@@ -1,91 +1,133 @@
{
"hash": "8745a243",
"configHash": "d74aae15",
"lockfileHash": "8786fedf",
"browserHash": "6bc53e11",
"hash": "ba7c7150",
"configHash": "6a220e5a",
"lockfileHash": "a10f8d29",
"browserHash": "db384cdc",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "0ccae341",
"fileHash": "d41cd819",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "b3aa3ef2",
"fileHash": "02c7f745",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "898bc344",
"fileHash": "beec6a15",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "49a33e04",
"fileHash": "f6c3e40e",
"needsInterop": true
},
"@tabler/icons-react": {
"src": "../../@tabler/icons-react/dist/esm/tabler-icons-react.mjs",
"file": "@tabler_icons-react.js",
"fileHash": "11d925b5",
"needsInterop": false
},
"appwrite": {
"src": "../../appwrite/dist/esm/sdk.js",
"file": "appwrite.js",
"fileHash": "1f73c770",
"fileHash": "e5052173",
"needsInterop": false
},
"clsx": {
"src": "../../clsx/dist/clsx.mjs",
"file": "clsx.js",
"fileHash": "7fc4f217",
"needsInterop": false
},
"date-fns": {
"src": "../../date-fns/esm/index.js",
"file": "date-fns.js",
"fileHash": "9fa70dfb",
"fileHash": "371588f0",
"needsInterop": false
},
"date-fns/locale": {
"src": "../../date-fns/esm/locale/index.js",
"file": "date-fns_locale.js",
"fileHash": "af0a0b06",
"fileHash": "927376cc",
"needsInterop": false
},
"motion/react": {
"src": "../../motion/dist/es/react.mjs",
"file": "motion_react.js",
"fileHash": "03c47f11",
"needsInterop": false
},
"postprocessing": {
"src": "../../postprocessing/build/index.js",
"file": "postprocessing.js",
"fileHash": "6a1af3d4",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "f6b57c12",
"fileHash": "13d89711",
"needsInterop": true
},
"react-icons/fa": {
"src": "../../react-icons/fa/index.esm.js",
"file": "react-icons_fa.js",
"fileHash": "750bb1d4",
"fileHash": "ce6891d0",
"needsInterop": false
},
"react-icons/fa6": {
"src": "../../react-icons/fa6/index.esm.js",
"file": "react-icons_fa6.js",
"fileHash": "e0a50b57",
"fileHash": "836a5ede",
"needsInterop": false
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.js",
"file": "react-router-dom.js",
"fileHash": "061d1fe8",
"fileHash": "65b7bd91",
"needsInterop": false
},
"tailwind-merge": {
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
"file": "tailwind-merge.js",
"fileHash": "8ea254d4",
"needsInterop": false
},
"three": {
"src": "../../three/build/three.module.js",
"file": "three.js",
"fileHash": "c500eef6",
"needsInterop": false
}
},
"chunks": {
"chunk-EHLE63A5": {
"file": "chunk-EHLE63A5.js"
"chunk-IFCYBMKG": {
"file": "chunk-IFCYBMKG.js"
},
"chunk-TOOCKHFL": {
"file": "chunk-TOOCKHFL.js"
"chunk-7VTGDDTZ": {
"file": "chunk-7VTGDDTZ.js"
},
"chunk-TYILIMWK": {
"file": "chunk-TYILIMWK.js"
"chunk-TDH2IRYZ": {
"file": "chunk-TDH2IRYZ.js"
},
"chunk-CANBAPAS": {
"file": "chunk-CANBAPAS.js"
"chunk-NMLHVZ76": {
"file": "chunk-NMLHVZ76.js"
},
"chunk-5WRI5ZAA": {
"file": "chunk-5WRI5ZAA.js"
"chunk-QRULMDK5": {
"file": "chunk-QRULMDK5.js"
},
"chunk-FSI7PPCM": {
"file": "chunk-FSI7PPCM.js"
},
"chunk-G3PMV62Z": {
"file": "chunk-G3PMV62Z.js"
}
}
}

View File

@@ -1,7 +1,7 @@
import {
__commonJS,
__toESM
} from "./chunk-5WRI5ZAA.js";
} from "./chunk-G3PMV62Z.js";
// node_modules/isomorphic-form-data/lib/browser.js
var require_browser = __commonJS({

File diff suppressed because one or more lines are too long

View File

@@ -1,31 +0,0 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
export {
__commonJS,
__toESM
};
//# sourceMappingURL=chunk-5WRI5ZAA.js.map

View File

@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,84 +0,0 @@
import {
require_react
} from "./chunk-CANBAPAS.js";
import {
__toESM
} from "./chunk-5WRI5ZAA.js";
// node_modules/react-icons/lib/esm/iconBase.js
var import_react2 = __toESM(require_react());
// node_modules/react-icons/lib/esm/iconContext.js
var import_react = __toESM(require_react());
var DefaultContext = {
color: void 0,
size: void 0,
className: void 0,
style: void 0,
attr: void 0
};
var IconContext = import_react.default.createContext && import_react.default.createContext(DefaultContext);
// node_modules/react-icons/lib/esm/iconBase.js
var __assign = function() {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __rest = function(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
}
return t;
};
function Tree2Element(tree) {
return tree && tree.map(function(node, i) {
return import_react2.default.createElement(node.tag, __assign({
key: i
}, node.attr), Tree2Element(node.child));
});
}
function GenIcon(data) {
return function(props) {
return import_react2.default.createElement(IconBase, __assign({
attr: __assign({}, data.attr)
}, props), Tree2Element(data.child));
};
}
function IconBase(props) {
var elem = function(conf) {
var attr = props.attr, size = props.size, title = props.title, svgProps = __rest(props, ["attr", "size", "title"]);
var computedSize = size || conf.size || "1em";
var className;
if (conf.className) className = conf.className;
if (props.className) className = (className ? className + " " : "") + props.className;
return import_react2.default.createElement("svg", __assign({
stroke: "currentColor",
fill: "currentColor",
strokeWidth: "0"
}, conf.attr, attr, svgProps, {
className,
style: __assign(__assign({
color: props.color || conf.color
}, conf.style), props.style),
height: computedSize,
width: computedSize,
xmlns: "http://www.w3.org/2000/svg"
}), title && import_react2.default.createElement("title", null, title), props.children);
};
return IconContext !== void 0 ? import_react2.default.createElement(IconContext.Consumer, null, function(conf) {
return elem(conf);
}) : elem(DefaultContext);
}
export {
GenIcon
};
//# sourceMappingURL=chunk-EHLE63A5.js.map

View File

@@ -1,7 +0,0 @@
{
"version": 3,
"sources": ["../../react-icons/lib/esm/iconBase.js", "../../react-icons/lib/esm/iconContext.js"],
"sourcesContent": ["var __assign = this && this.__assign || function () {\n __assign = Object.assign || function (t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n };\n return __assign.apply(this, arguments);\n};\nvar __rest = this && this.__rest || function (s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];\n }\n return t;\n};\nimport React from \"react\";\nimport { IconContext, DefaultContext } from \"./iconContext\";\nfunction Tree2Element(tree) {\n return tree && tree.map(function (node, i) {\n return React.createElement(node.tag, __assign({\n key: i\n }, node.attr), Tree2Element(node.child));\n });\n}\nexport function GenIcon(data) {\n // eslint-disable-next-line react/display-name\n return function (props) {\n return React.createElement(IconBase, __assign({\n attr: __assign({}, data.attr)\n }, props), Tree2Element(data.child));\n };\n}\nexport function IconBase(props) {\n var elem = function (conf) {\n var attr = props.attr,\n size = props.size,\n title = props.title,\n svgProps = __rest(props, [\"attr\", \"size\", \"title\"]);\n var computedSize = size || conf.size || \"1em\";\n var className;\n if (conf.className) className = conf.className;\n if (props.className) className = (className ? className + \" \" : \"\") + props.className;\n return React.createElement(\"svg\", __assign({\n stroke: \"currentColor\",\n fill: \"currentColor\",\n strokeWidth: \"0\"\n }, conf.attr, attr, svgProps, {\n className: className,\n style: __assign(__assign({\n color: props.color || conf.color\n }, conf.style), props.style),\n height: computedSize,\n width: computedSize,\n xmlns: \"http://www.w3.org/2000/svg\"\n }), title && React.createElement(\"title\", null, title), props.children);\n };\n return IconContext !== undefined ? React.createElement(IconContext.Consumer, null, function (conf) {\n return elem(conf);\n }) : elem(DefaultContext);\n}", "import React from \"react\";\nexport var DefaultContext = {\n color: undefined,\n size: undefined,\n className: undefined,\n style: undefined,\n attr: undefined\n};\nexport var IconContext = React.createContext && React.createContext(DefaultContext);"],
"mappings": ";;;;;;;;AAkBA,IAAAA,gBAAkB;;;AClBlB,mBAAkB;AACX,IAAI,iBAAiB;AAAA,EAC1B,OAAO;AAAA,EACP,MAAM;AAAA,EACN,WAAW;AAAA,EACX,OAAO;AAAA,EACP,MAAM;AACR;AACO,IAAI,cAAc,aAAAC,QAAM,iBAAiB,aAAAA,QAAM,cAAc,cAAc;;;ADRlF,IAAI,WAAoC,WAAY;AAClD,aAAW,OAAO,UAAU,SAAU,GAAG;AACvC,aAAS,GAAG,IAAI,GAAG,IAAI,UAAU,QAAQ,IAAI,GAAG,KAAK;AACnD,UAAI,UAAU,CAAC;AACf,eAAS,KAAK,EAAG,KAAI,OAAO,UAAU,eAAe,KAAK,GAAG,CAAC,EAAG,GAAE,CAAC,IAAI,EAAE,CAAC;AAAA,IAC7E;AACA,WAAO;AAAA,EACT;AACA,SAAO,SAAS,MAAM,MAAM,SAAS;AACvC;AACA,IAAI,SAAgC,SAAU,GAAG,GAAG;AAClD,MAAI,IAAI,CAAC;AACT,WAAS,KAAK,EAAG,KAAI,OAAO,UAAU,eAAe,KAAK,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAG,GAAE,CAAC,IAAI,EAAE,CAAC;AAC/F,MAAI,KAAK,QAAQ,OAAO,OAAO,0BAA0B,WAAY,UAAS,IAAI,GAAG,IAAI,OAAO,sBAAsB,CAAC,GAAG,IAAI,EAAE,QAAQ,KAAK;AAC3I,QAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,KAAK,OAAO,UAAU,qBAAqB,KAAK,GAAG,EAAE,CAAC,CAAC,EAAG,GAAE,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AAAA,EAClG;AACA,SAAO;AACT;AAGA,SAAS,aAAa,MAAM;AAC1B,SAAO,QAAQ,KAAK,IAAI,SAAU,MAAM,GAAG;AACzC,WAAO,cAAAC,QAAM,cAAc,KAAK,KAAK,SAAS;AAAA,MAC5C,KAAK;AAAA,IACP,GAAG,KAAK,IAAI,GAAG,aAAa,KAAK,KAAK,CAAC;AAAA,EACzC,CAAC;AACH;AACO,SAAS,QAAQ,MAAM;AAE5B,SAAO,SAAU,OAAO;AACtB,WAAO,cAAAA,QAAM,cAAc,UAAU,SAAS;AAAA,MAC5C,MAAM,SAAS,CAAC,GAAG,KAAK,IAAI;AAAA,IAC9B,GAAG,KAAK,GAAG,aAAa,KAAK,KAAK,CAAC;AAAA,EACrC;AACF;AACO,SAAS,SAAS,OAAO;AAC9B,MAAI,OAAO,SAAU,MAAM;AACzB,QAAI,OAAO,MAAM,MACf,OAAO,MAAM,MACb,QAAQ,MAAM,OACd,WAAW,OAAO,OAAO,CAAC,QAAQ,QAAQ,OAAO,CAAC;AACpD,QAAI,eAAe,QAAQ,KAAK,QAAQ;AACxC,QAAI;AACJ,QAAI,KAAK,UAAW,aAAY,KAAK;AACrC,QAAI,MAAM,UAAW,cAAa,YAAY,YAAY,MAAM,MAAM,MAAM;AAC5E,WAAO,cAAAA,QAAM,cAAc,OAAO,SAAS;AAAA,MACzC,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,aAAa;AAAA,IACf,GAAG,KAAK,MAAM,MAAM,UAAU;AAAA,MAC5B;AAAA,MACA,OAAO,SAAS,SAAS;AAAA,QACvB,OAAO,MAAM,SAAS,KAAK;AAAA,MAC7B,GAAG,KAAK,KAAK,GAAG,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,OAAO;AAAA,IACT,CAAC,GAAG,SAAS,cAAAA,QAAM,cAAc,SAAS,MAAM,KAAK,GAAG,MAAM,QAAQ;AAAA,EACxE;AACA,SAAO,gBAAgB,SAAY,cAAAA,QAAM,cAAc,YAAY,UAAU,MAAM,SAAU,MAAM;AACjG,WAAO,KAAK,IAAI;AAAA,EAClB,CAAC,IAAI,KAAK,cAAc;AAC1B;",
"names": ["import_react", "React", "React"]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -256,8 +256,8 @@ import {
weeksToDays,
yearsToMonths,
yearsToQuarters
} from "./chunk-TOOCKHFL.js";
import "./chunk-5WRI5ZAA.js";
} from "./chunk-FSI7PPCM.js";
import "./chunk-G3PMV62Z.js";
export {
add,
addBusinessDays,

View File

@@ -11,8 +11,8 @@ import {
requiredArgs,
startOfUTCWeek,
toDate
} from "./chunk-TOOCKHFL.js";
import "./chunk-5WRI5ZAA.js";
} from "./chunk-FSI7PPCM.js";
import "./chunk-G3PMV62Z.js";
// node_modules/date-fns/esm/locale/af/_lib/formatDistance/index.js
var formatDistanceLocale = {

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
import {
require_react_dom
} from "./chunk-TYILIMWK.js";
import "./chunk-CANBAPAS.js";
import "./chunk-5WRI5ZAA.js";
} from "./chunk-TDH2IRYZ.js";
import "./chunk-QRULMDK5.js";
import "./chunk-G3PMV62Z.js";
export default require_react_dom();
//# sourceMappingURL=react-dom.js.map

View File

@@ -1,10 +1,10 @@
import {
require_react_dom
} from "./chunk-TYILIMWK.js";
import "./chunk-CANBAPAS.js";
} from "./chunk-TDH2IRYZ.js";
import "./chunk-QRULMDK5.js";
import {
__commonJS
} from "./chunk-5WRI5ZAA.js";
} from "./chunk-G3PMV62Z.js";
// node_modules/react-dom/client.js
var require_client = __commonJS({

View File

@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../react-dom/client.js"],
"sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n"],
"sourcesContent": ["'use strict';\r\n\r\nvar m = require('react-dom');\r\nif (process.env.NODE_ENV === 'production') {\r\n exports.createRoot = m.createRoot;\r\n exports.hydrateRoot = m.hydrateRoot;\r\n} else {\r\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\r\n exports.createRoot = function(c, o) {\r\n i.usingClientEntryPoint = true;\r\n try {\r\n return m.createRoot(c, o);\r\n } finally {\r\n i.usingClientEntryPoint = false;\r\n }\r\n };\r\n exports.hydrateRoot = function(c, h, o) {\r\n i.usingClientEntryPoint = true;\r\n try {\r\n return m.hydrateRoot(c, h, o);\r\n } finally {\r\n i.usingClientEntryPoint = false;\r\n }\r\n };\r\n}\r\n"],
"mappings": ";;;;;;;;;AAAA;AAAA;AAEA,QAAI,IAAI;AACR,QAAI,OAAuC;AACzC,cAAQ,aAAa,EAAE;AACvB,cAAQ,cAAc,EAAE;AAAA,IAC1B,OAAO;AACD,UAAI,EAAE;AACV,cAAQ,aAAa,SAAS,GAAG,GAAG;AAClC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,WAAW,GAAG,CAAC;AAAA,QAC1B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AACA,cAAQ,cAAc,SAAS,GAAG,GAAG,GAAG;AACtC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,YAAY,GAAG,GAAG,CAAC;AAAA,QAC9B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAjBM;AAAA;AAAA;",
"names": []
}

View File

@@ -1,8 +1,8 @@
import {
GenIcon
} from "./chunk-EHLE63A5.js";
import "./chunk-CANBAPAS.js";
import "./chunk-5WRI5ZAA.js";
} from "./chunk-7VTGDDTZ.js";
import "./chunk-QRULMDK5.js";
import "./chunk-G3PMV62Z.js";
// node_modules/react-icons/fa/index.esm.js
function Fa500Px(props) {

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
import {
GenIcon
} from "./chunk-EHLE63A5.js";
import "./chunk-CANBAPAS.js";
import "./chunk-5WRI5ZAA.js";
} from "./chunk-7VTGDDTZ.js";
import "./chunk-QRULMDK5.js";
import "./chunk-G3PMV62Z.js";
// node_modules/react-icons/fa6/index.esm.js
function Fa42Group(props) {

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,12 @@
import {
require_react_dom
} from "./chunk-TYILIMWK.js";
} from "./chunk-TDH2IRYZ.js";
import {
require_react
} from "./chunk-CANBAPAS.js";
} from "./chunk-QRULMDK5.js";
import {
__toESM
} from "./chunk-5WRI5ZAA.js";
} from "./chunk-G3PMV62Z.js";
// node_modules/react-router-dom/dist/index.js
var React2 = __toESM(require_react());

4
node_modules/.vite/deps/react.js generated vendored
View File

@@ -1,6 +1,6 @@
import {
require_react
} from "./chunk-CANBAPAS.js";
import "./chunk-5WRI5ZAA.js";
} from "./chunk-QRULMDK5.js";
import "./chunk-G3PMV62Z.js";
export default require_react();
//# sourceMappingURL=react.js.map

View File

@@ -1,9 +1,9 @@
import {
require_react
} from "./chunk-CANBAPAS.js";
} from "./chunk-QRULMDK5.js";
import {
__commonJS
} from "./chunk-5WRI5ZAA.js";
} from "./chunk-G3PMV62Z.js";
// node_modules/react/cjs/react-jsx-dev-runtime.development.js
var require_react_jsx_dev_runtime_development = __commonJS({

File diff suppressed because one or more lines are too long

View File

@@ -1,925 +1,7 @@
import {
require_react
} from "./chunk-CANBAPAS.js";
import {
__commonJS
} from "./chunk-5WRI5ZAA.js";
// node_modules/react/cjs/react-jsx-runtime.development.js
var require_react_jsx_runtime_development = __commonJS({
"node_modules/react/cjs/react-jsx-runtime.development.js"(exports) {
"use strict";
if (true) {
(function() {
"use strict";
var React = require_react();
var REACT_ELEMENT_TYPE = Symbol.for("react.element");
var REACT_PORTAL_TYPE = Symbol.for("react.portal");
var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode");
var REACT_PROFILER_TYPE = Symbol.for("react.profiler");
var REACT_PROVIDER_TYPE = Symbol.for("react.provider");
var REACT_CONTEXT_TYPE = Symbol.for("react.context");
var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense");
var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list");
var REACT_MEMO_TYPE = Symbol.for("react.memo");
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen");
var MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
var FAUX_ITERATOR_SYMBOL = "@@iterator";
function getIteratorFn(maybeIterable) {
if (maybeIterable === null || typeof maybeIterable !== "object") {
return null;
}
var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];
if (typeof maybeIterator === "function") {
return maybeIterator;
}
return null;
}
var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
function error(format) {
{
{
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
printWarning("error", format, args);
}
}
}
function printWarning(level, format, args) {
{
var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame;
var stack = ReactDebugCurrentFrame2.getStackAddendum();
if (stack !== "") {
format += "%s";
args = args.concat([stack]);
}
var argsWithFormat = args.map(function(item) {
return String(item);
});
argsWithFormat.unshift("Warning: " + format);
Function.prototype.apply.call(console[level], console, argsWithFormat);
}
}
var enableScopeAPI = false;
var enableCacheElement = false;
var enableTransitionTracing = false;
var enableLegacyHidden = false;
var enableDebugTracing = false;
var REACT_MODULE_REFERENCE;
{
REACT_MODULE_REFERENCE = Symbol.for("react.module.reference");
}
function isValidElementType(type) {
if (typeof type === "string" || typeof type === "function") {
return true;
}
if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing) {
return true;
}
if (typeof type === "object" && type !== null) {
if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object
// types supported by any Flight configuration anywhere since
// we don't know which Flight build this will end up being used
// with.
type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== void 0) {
return true;
}
}
return false;
}
function getWrappedName(outerType, innerType, wrapperName) {
var displayName = outerType.displayName;
if (displayName) {
return displayName;
}
var functionName = innerType.displayName || innerType.name || "";
return functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName;
}
function getContextName(type) {
return type.displayName || "Context";
}
function getComponentNameFromType(type) {
if (type == null) {
return null;
}
{
if (typeof type.tag === "number") {
error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.");
}
}
if (typeof type === "function") {
return type.displayName || type.name || null;
}
if (typeof type === "string") {
return type;
}
switch (type) {
case REACT_FRAGMENT_TYPE:
return "Fragment";
case REACT_PORTAL_TYPE:
return "Portal";
case REACT_PROFILER_TYPE:
return "Profiler";
case REACT_STRICT_MODE_TYPE:
return "StrictMode";
case REACT_SUSPENSE_TYPE:
return "Suspense";
case REACT_SUSPENSE_LIST_TYPE:
return "SuspenseList";
}
if (typeof type === "object") {
switch (type.$$typeof) {
case REACT_CONTEXT_TYPE:
var context = type;
return getContextName(context) + ".Consumer";
case REACT_PROVIDER_TYPE:
var provider = type;
return getContextName(provider._context) + ".Provider";
case REACT_FORWARD_REF_TYPE:
return getWrappedName(type, type.render, "ForwardRef");
case REACT_MEMO_TYPE:
var outerName = type.displayName || null;
if (outerName !== null) {
return outerName;
}
return getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE: {
var lazyComponent = type;
var payload = lazyComponent._payload;
var init = lazyComponent._init;
try {
return getComponentNameFromType(init(payload));
} catch (x) {
return null;
}
}
}
}
return null;
}
var assign = Object.assign;
var disabledDepth = 0;
var prevLog;
var prevInfo;
var prevWarn;
var prevError;
var prevGroup;
var prevGroupCollapsed;
var prevGroupEnd;
function disabledLog() {
}
disabledLog.__reactDisabledLog = true;
function disableLogs() {
{
if (disabledDepth === 0) {
prevLog = console.log;
prevInfo = console.info;
prevWarn = console.warn;
prevError = console.error;
prevGroup = console.group;
prevGroupCollapsed = console.groupCollapsed;
prevGroupEnd = console.groupEnd;
var props = {
configurable: true,
enumerable: true,
value: disabledLog,
writable: true
};
Object.defineProperties(console, {
info: props,
log: props,
warn: props,
error: props,
group: props,
groupCollapsed: props,
groupEnd: props
});
}
disabledDepth++;
}
}
function reenableLogs() {
{
disabledDepth--;
if (disabledDepth === 0) {
var props = {
configurable: true,
enumerable: true,
writable: true
};
Object.defineProperties(console, {
log: assign({}, props, {
value: prevLog
}),
info: assign({}, props, {
value: prevInfo
}),
warn: assign({}, props, {
value: prevWarn
}),
error: assign({}, props, {
value: prevError
}),
group: assign({}, props, {
value: prevGroup
}),
groupCollapsed: assign({}, props, {
value: prevGroupCollapsed
}),
groupEnd: assign({}, props, {
value: prevGroupEnd
})
});
}
if (disabledDepth < 0) {
error("disabledDepth fell below zero. This is a bug in React. Please file an issue.");
}
}
}
var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
var prefix;
function describeBuiltInComponentFrame(name, source, ownerFn) {
{
if (prefix === void 0) {
try {
throw Error();
} catch (x) {
var match = x.stack.trim().match(/\n( *(at )?)/);
prefix = match && match[1] || "";
}
}
return "\n" + prefix + name;
}
}
var reentry = false;
var componentFrameCache;
{
var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map;
componentFrameCache = new PossiblyWeakMap();
}
function describeNativeComponentFrame(fn, construct) {
if (!fn || reentry) {
return "";
}
{
var frame = componentFrameCache.get(fn);
if (frame !== void 0) {
return frame;
}
}
var control;
reentry = true;
var previousPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = void 0;
var previousDispatcher;
{
previousDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = null;
disableLogs();
}
try {
if (construct) {
var Fake = function() {
throw Error();
};
Object.defineProperty(Fake.prototype, "props", {
set: function() {
throw Error();
}
});
if (typeof Reflect === "object" && Reflect.construct) {
try {
Reflect.construct(Fake, []);
} catch (x) {
control = x;
}
Reflect.construct(fn, [], Fake);
} else {
try {
Fake.call();
} catch (x) {
control = x;
}
fn.call(Fake.prototype);
}
} else {
try {
throw Error();
} catch (x) {
control = x;
}
fn();
}
} catch (sample) {
if (sample && control && typeof sample.stack === "string") {
var sampleLines = sample.stack.split("\n");
var controlLines = control.stack.split("\n");
var s = sampleLines.length - 1;
var c = controlLines.length - 1;
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
c--;
}
for (; s >= 1 && c >= 0; s--, c--) {
if (sampleLines[s] !== controlLines[c]) {
if (s !== 1 || c !== 1) {
do {
s--;
c--;
if (c < 0 || sampleLines[s] !== controlLines[c]) {
var _frame = "\n" + sampleLines[s].replace(" at new ", " at ");
if (fn.displayName && _frame.includes("<anonymous>")) {
_frame = _frame.replace("<anonymous>", fn.displayName);
}
{
if (typeof fn === "function") {
componentFrameCache.set(fn, _frame);
}
}
return _frame;
}
} while (s >= 1 && c >= 0);
}
break;
}
}
}
} finally {
reentry = false;
{
ReactCurrentDispatcher.current = previousDispatcher;
reenableLogs();
}
Error.prepareStackTrace = previousPrepareStackTrace;
}
var name = fn ? fn.displayName || fn.name : "";
var syntheticFrame = name ? describeBuiltInComponentFrame(name) : "";
{
if (typeof fn === "function") {
componentFrameCache.set(fn, syntheticFrame);
}
}
return syntheticFrame;
}
function describeFunctionComponentFrame(fn, source, ownerFn) {
{
return describeNativeComponentFrame(fn, false);
}
}
function shouldConstruct(Component) {
var prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}
function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) {
if (type == null) {
return "";
}
if (typeof type === "function") {
{
return describeNativeComponentFrame(type, shouldConstruct(type));
}
}
if (typeof type === "string") {
return describeBuiltInComponentFrame(type);
}
switch (type) {
case REACT_SUSPENSE_TYPE:
return describeBuiltInComponentFrame("Suspense");
case REACT_SUSPENSE_LIST_TYPE:
return describeBuiltInComponentFrame("SuspenseList");
}
if (typeof type === "object") {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE:
return describeFunctionComponentFrame(type.render);
case REACT_MEMO_TYPE:
return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);
case REACT_LAZY_TYPE: {
var lazyComponent = type;
var payload = lazyComponent._payload;
var init = lazyComponent._init;
try {
return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn);
} catch (x) {
}
}
}
}
return "";
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
var loggedTypeFailures = {};
var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
function setCurrentlyValidatingElement(element) {
{
if (element) {
var owner = element._owner;
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
ReactDebugCurrentFrame.setExtraStackFrame(stack);
} else {
ReactDebugCurrentFrame.setExtraStackFrame(null);
}
}
}
function checkPropTypes(typeSpecs, values, location, componentName, element) {
{
var has = Function.call.bind(hasOwnProperty);
for (var typeSpecName in typeSpecs) {
if (has(typeSpecs, typeSpecName)) {
var error$1 = void 0;
try {
if (typeof typeSpecs[typeSpecName] !== "function") {
var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
err.name = "Invariant Violation";
throw err;
}
error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
} catch (ex) {
error$1 = ex;
}
if (error$1 && !(error$1 instanceof Error)) {
setCurrentlyValidatingElement(element);
error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
setCurrentlyValidatingElement(null);
}
if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
loggedTypeFailures[error$1.message] = true;
setCurrentlyValidatingElement(element);
error("Failed %s type: %s", location, error$1.message);
setCurrentlyValidatingElement(null);
}
}
}
}
}
var isArrayImpl = Array.isArray;
function isArray(a) {
return isArrayImpl(a);
}
function typeName(value) {
{
var hasToStringTag = typeof Symbol === "function" && Symbol.toStringTag;
var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
return type;
}
}
function willCoercionThrow(value) {
{
try {
testStringCoercion(value);
return false;
} catch (e) {
return true;
}
}
}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
{
if (willCoercionThrow(value)) {
error("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value));
return testStringCoercion(value);
}
}
}
var ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
var RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
};
var specialPropKeyWarningShown;
var specialPropRefWarningShown;
var didWarnAboutStringRefs;
{
didWarnAboutStringRefs = {};
}
function hasValidRef(config) {
{
if (hasOwnProperty.call(config, "ref")) {
var getter = Object.getOwnPropertyDescriptor(config, "ref").get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.ref !== void 0;
}
function hasValidKey(config) {
{
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.key !== void 0;
}
function warnIfStringRefCannotBeAutoConverted(config, self) {
{
if (typeof config.ref === "string" && ReactCurrentOwner.current && self && ReactCurrentOwner.current.stateNode !== self) {
var componentName = getComponentNameFromType(ReactCurrentOwner.current.type);
if (!didWarnAboutStringRefs[componentName]) {
error('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref', getComponentNameFromType(ReactCurrentOwner.current.type), config.ref);
didWarnAboutStringRefs[componentName] = true;
}
}
}
}
function defineKeyPropWarningGetter(props, displayName) {
{
var warnAboutAccessingKey = function() {
if (!specialPropKeyWarningShown) {
specialPropKeyWarningShown = true;
error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
}
};
warnAboutAccessingKey.isReactWarning = true;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: true
});
}
}
function defineRefPropWarningGetter(props, displayName) {
{
var warnAboutAccessingRef = function() {
if (!specialPropRefWarningShown) {
specialPropRefWarningShown = true;
error("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
}
};
warnAboutAccessingRef.isReactWarning = true;
Object.defineProperty(props, "ref", {
get: warnAboutAccessingRef,
configurable: true
});
}
}
var ReactElement = function(type, key, ref, self, source, owner, props) {
var element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type,
key,
ref,
props,
// Record the component responsible for creating this element.
_owner: owner
};
{
element._store = {};
Object.defineProperty(element._store, "validated", {
configurable: false,
enumerable: false,
writable: true,
value: false
});
Object.defineProperty(element, "_self", {
configurable: false,
enumerable: false,
writable: false,
value: self
});
Object.defineProperty(element, "_source", {
configurable: false,
enumerable: false,
writable: false,
value: source
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
function jsxDEV(type, config, maybeKey, source, self) {
{
var propName;
var props = {};
var key = null;
var ref = null;
if (maybeKey !== void 0) {
{
checkKeyStringCoercion(maybeKey);
}
key = "" + maybeKey;
}
if (hasValidKey(config)) {
{
checkKeyStringCoercion(config.key);
}
key = "" + config.key;
}
if (hasValidRef(config)) {
ref = config.ref;
warnIfStringRefCannotBeAutoConverted(config, self);
}
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === void 0) {
props[propName] = defaultProps[propName];
}
}
}
if (key || ref) {
var displayName = typeof type === "function" ? type.displayName || type.name || "Unknown" : type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
}
var ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner;
var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;
function setCurrentlyValidatingElement$1(element) {
{
if (element) {
var owner = element._owner;
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
ReactDebugCurrentFrame$1.setExtraStackFrame(stack);
} else {
ReactDebugCurrentFrame$1.setExtraStackFrame(null);
}
}
}
var propTypesMisspellWarningShown;
{
propTypesMisspellWarningShown = false;
}
function isValidElement(object) {
{
return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}
}
function getDeclarationErrorAddendum() {
{
if (ReactCurrentOwner$1.current) {
var name = getComponentNameFromType(ReactCurrentOwner$1.current.type);
if (name) {
return "\n\nCheck the render method of `" + name + "`.";
}
}
return "";
}
}
function getSourceInfoErrorAddendum(source) {
{
if (source !== void 0) {
var fileName = source.fileName.replace(/^.*[\\\/]/, "");
var lineNumber = source.lineNumber;
return "\n\nCheck your code at " + fileName + ":" + lineNumber + ".";
}
return "";
}
}
var ownerHasKeyUseWarning = {};
function getCurrentComponentErrorInfo(parentType) {
{
var info = getDeclarationErrorAddendum();
if (!info) {
var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name;
if (parentName) {
info = "\n\nCheck the top-level render call using <" + parentName + ">.";
}
}
return info;
}
}
function validateExplicitKey(element, parentType) {
{
if (!element._store || element._store.validated || element.key != null) {
return;
}
element._store.validated = true;
var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {
return;
}
ownerHasKeyUseWarning[currentComponentErrorInfo] = true;
var childOwner = "";
if (element && element._owner && element._owner !== ReactCurrentOwner$1.current) {
childOwner = " It was passed a child from " + getComponentNameFromType(element._owner.type) + ".";
}
setCurrentlyValidatingElement$1(element);
error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner);
setCurrentlyValidatingElement$1(null);
}
}
function validateChildKeys(node, parentType) {
{
if (typeof node !== "object") {
return;
}
if (isArray(node)) {
for (var i = 0; i < node.length; i++) {
var child = node[i];
if (isValidElement(child)) {
validateExplicitKey(child, parentType);
}
}
} else if (isValidElement(node)) {
if (node._store) {
node._store.validated = true;
}
} else if (node) {
var iteratorFn = getIteratorFn(node);
if (typeof iteratorFn === "function") {
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;
while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
}
}
}
function validatePropTypes(element) {
{
var type = element.type;
if (type === null || type === void 0 || typeof type === "string") {
return;
}
var propTypes;
if (typeof type === "function") {
propTypes = type.propTypes;
} else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here.
// Inner props are checked in the reconciler.
type.$$typeof === REACT_MEMO_TYPE)) {
propTypes = type.propTypes;
} else {
return;
}
if (propTypes) {
var name = getComponentNameFromType(type);
checkPropTypes(propTypes, element.props, "prop", name, element);
} else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) {
propTypesMisspellWarningShown = true;
var _name = getComponentNameFromType(type);
error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown");
}
if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) {
error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead.");
}
}
}
function validateFragmentProps(fragment) {
{
var keys = Object.keys(fragment.props);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key !== "children" && key !== "key") {
setCurrentlyValidatingElement$1(fragment);
error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key);
setCurrentlyValidatingElement$1(null);
break;
}
}
if (fragment.ref !== null) {
setCurrentlyValidatingElement$1(fragment);
error("Invalid attribute `ref` supplied to `React.Fragment`.");
setCurrentlyValidatingElement$1(null);
}
}
}
var didWarnAboutKeySpread = {};
function jsxWithValidation(type, props, key, isStaticChildren, source, self) {
{
var validType = isValidElementType(type);
if (!validType) {
var info = "";
if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) {
info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.";
}
var sourceInfo = getSourceInfoErrorAddendum(source);
if (sourceInfo) {
info += sourceInfo;
} else {
info += getDeclarationErrorAddendum();
}
var typeString;
if (type === null) {
typeString = "null";
} else if (isArray(type)) {
typeString = "array";
} else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) {
typeString = "<" + (getComponentNameFromType(type.type) || "Unknown") + " />";
info = " Did you accidentally export a JSX literal instead of a component?";
} else {
typeString = typeof type;
}
error("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info);
}
var element = jsxDEV(type, props, key, source, self);
if (element == null) {
return element;
}
if (validType) {
var children = props.children;
if (children !== void 0) {
if (isStaticChildren) {
if (isArray(children)) {
for (var i = 0; i < children.length; i++) {
validateChildKeys(children[i], type);
}
if (Object.freeze) {
Object.freeze(children);
}
} else {
error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
}
} else {
validateChildKeys(children, type);
}
}
}
{
if (hasOwnProperty.call(props, "key")) {
var componentName = getComponentNameFromType(type);
var keys = Object.keys(props).filter(function(k) {
return k !== "key";
});
var beforeExample = keys.length > 0 ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
if (!didWarnAboutKeySpread[componentName + beforeExample]) {
var afterExample = keys.length > 0 ? "{" + keys.join(": ..., ") + ": ...}" : "{}";
error('A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />', beforeExample, componentName, afterExample, componentName);
didWarnAboutKeySpread[componentName + beforeExample] = true;
}
}
}
if (type === REACT_FRAGMENT_TYPE) {
validateFragmentProps(element);
} else {
validatePropTypes(element);
}
return element;
}
}
function jsxWithValidationStatic(type, props, key) {
{
return jsxWithValidation(type, props, key, true);
}
}
function jsxWithValidationDynamic(type, props, key) {
{
return jsxWithValidation(type, props, key, false);
}
}
var jsx = jsxWithValidationDynamic;
var jsxs = jsxWithValidationStatic;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.jsx = jsx;
exports.jsxs = jsxs;
})();
}
}
});
// node_modules/react/jsx-runtime.js
var require_jsx_runtime = __commonJS({
"node_modules/react/jsx-runtime.js"(exports, module) {
if (false) {
module.exports = null;
} else {
module.exports = require_react_jsx_runtime_development();
}
}
});
require_jsx_runtime
} from "./chunk-NMLHVZ76.js";
import "./chunk-QRULMDK5.js";
import "./chunk-G3PMV62Z.js";
export default require_jsx_runtime();
/*! Bundled license information:
react/cjs/react-jsx-runtime.development.js:
(**
* @license React
* react-jsx-runtime.development.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
*/
//# sourceMappingURL=react_jsx-runtime.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
# esbuild
This is the macOS ARM 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.

Binary file not shown.

View File

@@ -1,20 +0,0 @@
{
"name": "@esbuild/darwin-arm64",
"version": "0.21.5",
"description": "The macOS ARM 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=12"
},
"os": [
"darwin"
],
"cpu": [
"arm64"
]
}

View File

@@ -1,3 +0,0 @@
# `@rollup/rollup-darwin-arm64`
This is the **aarch64-apple-darwin** binary for `rollup`

View File

@@ -1,22 +0,0 @@
{
"name": "@rollup/rollup-darwin-arm64",
"version": "4.53.5",
"os": [
"darwin"
],
"cpu": [
"arm64"
],
"files": [
"rollup.darwin-arm64.node"
],
"description": "Native bindings for Rollup",
"author": "Lukas Taegert-Atkinson",
"homepage": "https://rollupjs.org/",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/rollup/rollup.git"
},
"main": "./rollup.darwin-arm64.node"
}

BIN
node_modules/esbuild/bin/esbuild generated vendored

Binary file not shown.

22
node_modules/fsevents/LICENSE generated vendored
View File

@@ -1,22 +0,0 @@
MIT License
-----------
Copyright (C) 2010-2020 by Philipp Dunkel, Ben Noordhuis, Elan Shankar, Paul Miller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

89
node_modules/fsevents/README.md generated vendored
View File

@@ -1,89 +0,0 @@
# fsevents
Native access to MacOS FSEvents in [Node.js](https://nodejs.org/)
The FSEvents API in MacOS allows applications to register for notifications of
changes to a given directory tree. It is a very fast and lightweight alternative
to kqueue.
This is a low-level library. For a cross-platform file watching module that
uses fsevents, check out [Chokidar](https://github.com/paulmillr/chokidar).
## Usage
```sh
npm install fsevents
```
Supports only **Node.js v8.16 and higher**.
```js
const fsevents = require('fsevents');
// To start observation
const stop = fsevents.watch(__dirname, (path, flags, id) => {
const info = fsevents.getInfo(path, flags);
});
// To end observation
stop();
```
> **Important note:** The API behaviour is slightly different from typical JS APIs. The `stop` function **must** be
> retrieved and stored somewhere, even if you don't plan to stop the watcher. If you forget it, the garbage collector
> will eventually kick in, the watcher will be unregistered, and your callbacks won't be called anymore.
The callback passed as the second parameter to `.watch` get's called whenever the operating system detects a
a change in the file system. It takes three arguments:
###### `fsevents.watch(dirname: string, (path: string, flags: number, id: string) => void): () => Promise<undefined>`
* `path: string` - the item in the filesystem that have been changed
* `flags: number` - a numeric value describing what the change was
* `id: string` - an unique-id identifying this specific event
Returns closer callback which when called returns a Promise resolving when the watcher process has been shut down.
###### `fsevents.getInfo(path: string, flags: number, id: string): FsEventInfo`
The `getInfo` function takes the `path`, `flags` and `id` arguments and converts those parameters into a structure
that is easier to digest to determine what the change was.
The `FsEventsInfo` has the following shape:
```js
/**
* @typedef {'created'|'modified'|'deleted'|'moved'|'root-changed'|'cloned'|'unknown'} FsEventsEvent
* @typedef {'file'|'directory'|'symlink'} FsEventsType
*/
{
"event": "created", // {FsEventsEvent}
"path": "file.txt",
"type": "file", // {FsEventsType}
"changes": {
"inode": true, // Had iNode Meta-Information changed
"finder": false, // Had Finder Meta-Data changed
"access": false, // Had access permissions changed
"xattrs": false // Had xAttributes changed
},
"flags": 0x100000000
}
```
## Changelog
- v2.3 supports Apple Silicon ARM CPUs
- v2 supports node 8.16+ and reduces package size massively
- v1.2.8 supports node 6+
- v1.2.7 supports node 4+
## Troubleshooting
- I'm getting `EBADPLATFORM` `Unsupported platform for fsevents` error.
- It's fine, nothing is broken. fsevents is macos-only. Other platforms are skipped. If you want to hide this warning, report a bug to NPM bugtracker asking them to hide ebadplatform warnings by default.
## License
The MIT License Copyright (C) 2010-2020 by Philipp Dunkel, Ben Noordhuis, Elan Shankar, Paul Miller — see LICENSE file.
Visit our [GitHub page](https://github.com/fsevents/fsevents) and [NPM Page](https://npmjs.org/package/fsevents)

46
node_modules/fsevents/fsevents.d.ts generated vendored
View File

@@ -1,46 +0,0 @@
declare type Event = "created" | "cloned" | "modified" | "deleted" | "moved" | "root-changed" | "unknown";
declare type Type = "file" | "directory" | "symlink";
declare type FileChanges = {
inode: boolean;
finder: boolean;
access: boolean;
xattrs: boolean;
};
declare type Info = {
event: Event;
path: string;
type: Type;
changes: FileChanges;
flags: number;
};
declare type WatchHandler = (path: string, flags: number, id: string) => void;
export declare function watch(path: string, handler: WatchHandler): () => Promise<void>;
export declare function watch(path: string, since: number, handler: WatchHandler): () => Promise<void>;
export declare function getInfo(path: string, flags: number): Info;
export declare const constants: {
None: 0x00000000;
MustScanSubDirs: 0x00000001;
UserDropped: 0x00000002;
KernelDropped: 0x00000004;
EventIdsWrapped: 0x00000008;
HistoryDone: 0x00000010;
RootChanged: 0x00000020;
Mount: 0x00000040;
Unmount: 0x00000080;
ItemCreated: 0x00000100;
ItemRemoved: 0x00000200;
ItemInodeMetaMod: 0x00000400;
ItemRenamed: 0x00000800;
ItemModified: 0x00001000;
ItemFinderInfoMod: 0x00002000;
ItemChangeOwner: 0x00004000;
ItemXattrMod: 0x00008000;
ItemIsFile: 0x00010000;
ItemIsDir: 0x00020000;
ItemIsSymlink: 0x00040000;
ItemIsHardlink: 0x00100000;
ItemIsLastHardlink: 0x00200000;
OwnEvent: 0x00080000;
ItemCloned: 0x00400000;
};
export {};

83
node_modules/fsevents/fsevents.js generated vendored
View File

@@ -1,83 +0,0 @@
/*
** © 2020 by Philipp Dunkel, Ben Noordhuis, Elan Shankar, Paul Miller
** Licensed under MIT License.
*/
/* jshint node:true */
"use strict";
if (process.platform !== "darwin") {
throw new Error(`Module 'fsevents' is not compatible with platform '${process.platform}'`);
}
const Native = require("./fsevents.node");
const events = Native.constants;
function watch(path, since, handler) {
if (typeof path !== "string") {
throw new TypeError(`fsevents argument 1 must be a string and not a ${typeof path}`);
}
if ("function" === typeof since && "undefined" === typeof handler) {
handler = since;
since = Native.flags.SinceNow;
}
if (typeof since !== "number") {
throw new TypeError(`fsevents argument 2 must be a number and not a ${typeof since}`);
}
if (typeof handler !== "function") {
throw new TypeError(`fsevents argument 3 must be a function and not a ${typeof handler}`);
}
let instance = Native.start(Native.global, path, since, handler);
if (!instance) throw new Error(`could not watch: ${path}`);
return () => {
const result = instance ? Promise.resolve(instance).then(Native.stop) : Promise.resolve(undefined);
instance = undefined;
return result;
};
}
function getInfo(path, flags) {
return {
path,
flags,
event: getEventType(flags),
type: getFileType(flags),
changes: getFileChanges(flags),
};
}
function getFileType(flags) {
if (events.ItemIsFile & flags) return "file";
if (events.ItemIsDir & flags) return "directory";
if (events.MustScanSubDirs & flags) return "directory";
if (events.ItemIsSymlink & flags) return "symlink";
}
function anyIsTrue(obj) {
for (let key in obj) {
if (obj[key]) return true;
}
return false;
}
function getEventType(flags) {
if (events.ItemRemoved & flags) return "deleted";
if (events.ItemRenamed & flags) return "moved";
if (events.ItemCreated & flags) return "created";
if (events.ItemModified & flags) return "modified";
if (events.RootChanged & flags) return "root-changed";
if (events.ItemCloned & flags) return "cloned";
if (anyIsTrue(flags)) return "modified";
return "unknown";
}
function getFileChanges(flags) {
return {
inode: !!(events.ItemInodeMetaMod & flags),
finder: !!(events.ItemFinderInfoMod & flags),
access: !!(events.ItemChangeOwner & flags),
xattrs: !!(events.ItemXattrMod & flags),
};
}
exports.watch = watch;
exports.getInfo = getInfo;
exports.constants = events;

BIN
node_modules/fsevents/fsevents.node generated vendored

Binary file not shown.

62
node_modules/fsevents/package.json generated vendored
View File

@@ -1,62 +0,0 @@
{
"name": "fsevents",
"version": "2.3.3",
"description": "Native Access to MacOS FSEvents",
"main": "fsevents.js",
"types": "fsevents.d.ts",
"os": [
"darwin"
],
"files": [
"fsevents.d.ts",
"fsevents.js",
"fsevents.node"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
},
"scripts": {
"clean": "node-gyp clean && rm -f fsevents.node",
"build": "node-gyp clean && rm -f fsevents.node && node-gyp rebuild && node-gyp clean",
"test": "/bin/bash ./test.sh 2>/dev/null",
"prepublishOnly": "npm run build"
},
"repository": {
"type": "git",
"url": "https://github.com/fsevents/fsevents.git"
},
"keywords": [
"fsevents",
"mac"
],
"contributors": [
{
"name": "Philipp Dunkel",
"email": "pip@pipobscure.com"
},
{
"name": "Ben Noordhuis",
"email": "info@bnoordhuis.nl"
},
{
"name": "Elan Shankar",
"email": "elan.shanker@gmail.com"
},
{
"name": "Miroslav Bajtoš",
"email": "mbajtoss@gmail.com"
},
{
"name": "Paul Miller",
"url": "https://paulmillr.com"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fsevents/fsevents/issues"
},
"homepage": "https://github.com/fsevents/fsevents",
"devDependencies": {
"node-gyp": "^9.4.0"
}
}

149
package-lock.json generated
View File

@@ -8,12 +8,18 @@
"name": "woms-react",
"version": "1.0.0",
"dependencies": {
"@tabler/icons-react": "^3.36.0",
"appwrite": "^13.0.0",
"clsx": "^2.1.1",
"date-fns": "^2.30.0",
"motion": "^12.23.26",
"postprocessing": "^6.38.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0",
"react-router-dom": "^6.20.0"
"react-router-dom": "^6.20.0",
"tailwind-merge": "^3.4.0",
"three": "^0.182.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
@@ -50,6 +56,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1006,6 +1013,32 @@
"win32"
]
},
"node_modules/@tabler/icons": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.0.tgz",
"integrity": "sha512-z9OfTEG6QbaQWM9KBOxxUdpgvMUn0atageXyiaSc2gmYm51ORO8Ua7eUcjlks+Dc0YMK4rrodAFdK9SfjJ4ZcA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
}
},
"node_modules/@tabler/icons-react": {
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.36.0.tgz",
"integrity": "sha512-sSZ00bEjTdTTskVFykq294RJq+9cFatwy4uYa78HcYBCXU1kSD1DIp5yoFsQXmybkIOKCjp18OnhAYk553UIfQ==",
"license": "MIT",
"dependencies": {
"@tabler/icons": "3.36.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
},
"peerDependencies": {
"react": ">= 16"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1064,6 +1097,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -1140,6 +1174,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -1186,6 +1221,15 @@
}
]
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1380,6 +1424,33 @@
"node": ">= 0.12"
}
},
"node_modules/framer-motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1577,6 +1648,47 @@
"node": ">= 0.6"
}
},
"node_modules/motion": {
"version": "12.23.26",
"resolved": "https://registry.npmjs.org/motion/-/motion-12.23.26.tgz",
"integrity": "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==",
"license": "MIT",
"dependencies": {
"framer-motion": "^12.23.26",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1660,10 +1772,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postprocessing": {
"version": "6.38.2",
"resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.38.2.tgz",
"integrity": "sha512-7DwuT7Tkst41ZjSj287g7C9c5/D3Xx5rMgBosg0dadbUPoZD2HNzkadKPol1d2PJAoI9f+Jeh1/v9YfLzpFGVw==",
"license": "Zlib",
"peerDependencies": {
"three": ">= 0.157.0 < 0.183.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -1675,6 +1797,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -1816,11 +1939,34 @@
"node": ">=0.10.0"
}
},
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/three": {
"version": "0.182.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
"license": "MIT",
"peer": true
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -1856,6 +2002,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@@ -3,18 +3,24 @@
"version": "1.0.0",
"private": true,
"dependencies": {
"@tabler/icons-react": "^3.36.0",
"appwrite": "^13.0.0",
"clsx": "^2.1.1",
"date-fns": "^2.30.0",
"motion": "^12.23.26",
"postprocessing": "^6.38.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"appwrite": "^13.0.0",
"react-icons": "^4.12.0",
"date-fns": "^2.30.0"
"react-router-dom": "^6.20.0",
"tailwind-merge": "^3.4.0",
"three": "^0.182.0"
},
"devDependencies": {
"vite": "^5.0.0",
"@vitejs/plugin-react": "^4.2.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0"
},
"scripts": {
"dev": "vite",

View File

@@ -2,6 +2,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './context/AuthContext'
import Navbar from './components/Navbar'
import Footer from './components/Footer'
import PixelBlast from './components/PixelBlast'
import LoginPage from './pages/LoginPage'
import TicketsPage from './pages/TicketsPage'
import DashboardPage from './pages/DashboardPage'
@@ -10,6 +11,7 @@ import PlanboardPage from './pages/PlanboardPage'
import ProjectsPage from './pages/ProjectsPage'
import ReportsPage from './pages/ReportsPage'
import DocsPage from './pages/DocsPage'
import AdminPage from './pages/AdminPage'
function ProtectedRoute({ children }) {
const { user, loading } = useAuth()
@@ -44,6 +46,7 @@ function AppRoutes() {
<Route path="/projects" element={<ProtectedRoute><ProjectsPage /></ProtectedRoute>} />
<Route path="/reports" element={<ProtectedRoute><ReportsPage /></ProtectedRoute>} />
<Route path="/docs" element={<ProtectedRoute><DocsPage /></ProtectedRoute>} />
<Route path="/admin" element={<ProtectedRoute><AdminPage /></ProtectedRoute>} />
</Routes>
)
}
@@ -51,11 +54,72 @@ function AppRoutes() {
function AppContent() {
const { user } = useAuth()
if (!user) {
return (
<>
{/* PixelBlast Background für Login-Seite */}
<div style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 0,
background: '#1a202c'
}}>
<PixelBlast
color="#10b981"
variant="circle"
pixelSize={4}
patternScale={2}
patternDensity={0.8}
enableRipples={true}
rippleIntensityScale={1.5}
speed={0.3}
edgeFade={0.3}
/>
</div>
<div style={{ position: 'relative', zIndex: 1 }}>
<AppRoutes />
</div>
</>
)
}
return (
<>
{user && <Navbar />}
<AppRoutes />
{user && <Footer />}
{/* PixelBlast Background für Haupt-App */}
<div style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 0,
background: '#1a202c'
}}>
<PixelBlast
color="#10b981"
variant="circle"
pixelSize={4}
patternScale={2}
patternDensity={0.8}
enableRipples={true}
rippleIntensityScale={1.5}
speed={0.3}
edgeFade={0.3}
/>
</div>
<div style={{ position: 'relative', zIndex: 1, display: 'flex', height: '100vh', overflow: 'hidden' }}>
<Navbar />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ flex: 1, overflowY: 'auto' }}>
<AppRoutes />
</div>
<Footer />
</div>
</div>
</>
)
}

View File

@@ -1,28 +1,31 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useAdminConfig } from '../hooks/useAdminConfig'
import { useEmployees } from '../hooks/useEmployees'
const TICKET_TYPES = [
// Fallback-Werte falls Config nicht geladen werden kann
const DEFAULT_TICKET_TYPES = [
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
'Rollout', 'Emergency Call', 'Other Services'
]
const SYSTEMS = [
const DEFAULT_SYSTEMS = [
'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware',
'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network',
'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking',
'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a'
]
const RESPONSE_LEVELS = [
const DEFAULT_RESPONSE_LEVELS = [
'USER', 'KEY USER', 'Helpdesk', 'Support', 'Admin', 'FS/FE', '24/7',
'TECH MGMT', 'Backoffice', 'BUSI MGMT', 'n/a'
]
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site']
const DEFAULT_SERVICE_TYPES = ['Remote', 'On Site', 'Off Site']
const PRIORITIES = [
const DEFAULT_PRIORITIES = [
{ value: 0, label: 'None' },
{ value: 1, label: 'Low' },
{ value: 2, label: 'Medium' },
@@ -33,16 +36,27 @@ const PRIORITIES = [
const today = new Date().toLocaleDateString('de-DE')
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
const { config } = useAdminConfig()
const { employees } = useEmployees()
// Verwende Config-Werte oder Fallbacks
const TICKET_TYPES = config?.ticketTypes || DEFAULT_TICKET_TYPES
const SYSTEMS = config?.systems || DEFAULT_SYSTEMS
const RESPONSE_LEVELS = config?.responseLevels || DEFAULT_RESPONSE_LEVELS
const SERVICE_TYPES = config?.serviceTypes || DEFAULT_SERVICE_TYPES
const PRIORITIES = config?.priorities || DEFAULT_PRIORITIES
const [formData, setFormData] = useState({
customerId: '',
type: 'Supportrequest',
type: '',
systemType: '',
responseLevel: '',
serviceType: 'Remote',
serviceType: '',
priority: 1,
topic: '',
requestedBy: '',
requestedFor: '',
assignedTo: '', // Zugewiesener Mitarbeiter (User ID)
status: 'Open', // Status wird automatisch gesetzt basierend auf assignedTo
startDate: today,
startTime: '',
deadline: today,
@@ -54,39 +68,64 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
// Setze Default-Werte wenn Config geladen ist oder Modal geöffnet wird
useEffect(() => {
if (isOpen && (TICKET_TYPES.length > 0 || SERVICE_TYPES.length > 0)) {
setFormData(prev => ({
...prev,
type: prev.type || TICKET_TYPES[0] || 'Supportrequest',
serviceType: prev.serviceType || SERVICE_TYPES[0] || 'Remote',
priority: prev.priority || (PRIORITIES.find(p => p.value === 1)?.value || 1)
}))
}
// Reset error when modal opens
if (isOpen) {
setError('')
}
}, [isOpen, TICKET_TYPES, SERVICE_TYPES, PRIORITIES])
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }))
// Clear error when user makes changes
if (error) setError('')
}
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError('')
try {
await onCreate(formData)
onClose()
setFormData({
customerId: '',
type: 'Supportrequest',
systemType: '',
responseLevel: '',
serviceType: 'Remote',
priority: 1,
topic: '',
requestedBy: '',
requestedFor: '',
startDate: today,
startTime: '',
deadline: today,
endTime: '',
estimate: '30',
mailCopyTo: '',
sendNotification: false,
details: ''
})
const result = await onCreate(formData)
if (result.success) {
onClose()
setFormData({
customerId: '',
type: TICKET_TYPES[0] || 'Supportrequest',
systemType: '',
responseLevel: '',
serviceType: SERVICE_TYPES[0] || 'Remote',
priority: PRIORITIES.find(p => p.value === 1)?.value || 1,
topic: '',
requestedBy: '',
requestedFor: '',
startDate: today,
startTime: '',
deadline: today,
endTime: '',
estimate: '30',
mailCopyTo: '',
sendNotification: false,
details: ''
})
} else {
setError(result.error || 'Fehler beim Erstellen des Tickets')
}
} catch (error) {
console.error('Error creating ticket:', error)
setError(error.message || 'Ein unerwarteter Fehler ist aufgetreten')
} finally {
setLoading(false)
}
@@ -102,6 +141,12 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
<div className="overlay-content">
<h2 className="mb-2">Create New Ticket</h2>
{error && (
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col col-6">
@@ -115,7 +160,7 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
>
<option value="">Affected Customer</option>
{customers.map(c => (
<option key={c.id} value={c.id}>({c.code}) {c.name}</option>
<option key={c.$id} value={c.$id}>({c.code || ''}) {c.name || 'Unnamed'}</option>
))}
</select>
</div>
@@ -189,6 +234,32 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
))}
</select>
</div>
<div className="form-group">
<label className="form-label">Assigned To</label>
<select
className="form-control"
value={formData.assignedTo}
onChange={(e) => {
const userId = e.target.value
handleChange('assignedTo', userId)
// Status-Automatik: Wenn Mitarbeiter zugewiesen → Status = "Assigned"
// Wenn kein Mitarbeiter → Status = "Open"
if (userId) {
handleChange('status', 'Assigned')
} else {
handleChange('status', 'Open')
}
}}
>
<option value="">Unassigned</option>
{employees.map(emp => (
<option key={emp.$id} value={emp.userId}>
{emp.displayName}{emp.shortcode ? ` (${emp.shortcode})` : ''}
</option>
))}
</select>
</div>
</div>
<div className="col col-6">

View File

@@ -0,0 +1,327 @@
import { useState, useEffect } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useAuth } from '../context/AuthContext'
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site', 'COMMENT']
const STATUS_OPTIONS = [
'Open',
'Closed',
'Awaiting',
'Added Info',
'Occupied',
'Halted',
'Cancelled',
'Aborted',
'Assigned',
'In Test'
]
const RESPONSE_LEVELS = [
'KEY USER',
'1st Level',
'2nd Level',
'3rd Level',
'FS/FE',
'24/7',
'TECH MGMT',
'Backoffice',
'BUSI MGMT',
'n/a'
]
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate }) {
const { user } = useAuth()
const today = new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
const [formData, setFormData] = useState({
serviceType: 'Remote',
newStatus: workorder?.status || 'Open',
newResponseLevel: workorder?.responseLevel || '',
totalTime: 0,
startDate: today,
startTime: '',
endDate: today,
endTime: '',
details: '',
isComment: false
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [autoCalculate, setAutoCalculate] = useState(true)
// Reset form wenn Modal geöffnet wird
useEffect(() => {
if (isOpen && workorder) {
setFormData({
serviceType: 'Remote',
newStatus: workorder.status || 'Open',
newResponseLevel: workorder.responseLevel || '',
totalTime: 0,
startDate: today,
startTime: '',
endDate: today,
endTime: '',
details: '',
isComment: false
})
setError('')
setAutoCalculate(true)
}
}, [isOpen, workorder, today])
// Automatische Zeitberechnung
useEffect(() => {
if (autoCalculate && formData.startTime && formData.endTime && !formData.isComment) {
try {
const startHour = parseInt(formData.startTime.substring(0, 2))
const startMin = parseInt(formData.startTime.substring(2, 4))
const endHour = parseInt(formData.endTime.substring(0, 2))
const endMin = parseInt(formData.endTime.substring(2, 4))
if (!isNaN(startHour) && !isNaN(startMin) && !isNaN(endHour) && !isNaN(endMin)) {
const startTotal = startHour * 60 + startMin
const endTotal = endHour * 60 + endMin
let diff = endTotal - startTotal
if (diff < 0) {
diff += 24 * 60 // Overnight
}
setFormData(prev => ({ ...prev, totalTime: diff }))
}
} catch (err) {
// Ignoriere Fehler
}
}
}, [formData.startTime, formData.endTime, formData.isComment, autoCalculate])
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }))
// Wenn totalTime manuell geändert wird, deaktiviere Auto-Berechnung
if (field === 'totalTime') {
setAutoCalculate(false)
}
}
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError('')
try {
if (!formData.details.trim()) {
setError('Bitte Details eingeben')
setLoading(false)
return
}
const worksheetData = {
woid: workorder.woid,
workorderId: workorder.$id,
serviceType: formData.serviceType,
oldStatus: workorder.status,
newStatus: formData.newStatus,
oldResponseLevel: workorder.responseLevel || '',
newResponseLevel: formData.newResponseLevel,
totalTime: formData.isComment ? 0 : parseInt(formData.totalTime) || 0,
startDate: formData.startDate,
startTime: formData.startTime,
endDate: formData.endDate,
endTime: formData.endTime,
details: formData.details,
isComment: formData.isComment,
employeeShort: user?.prefs?.shortCode || '' // Aus User-Preferences
}
const result = await onCreate(worksheetData, user)
if (result.success) {
onClose()
} else {
setError(result.error || 'Fehler beim Erstellen des Worksheets')
}
} catch (err) {
console.error('Error creating worksheet:', err)
setError(err.message || 'Ein unerwarteter Fehler ist aufgetreten')
} finally {
setLoading(false)
}
}
if (!isOpen || !workorder) return null
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content">
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
{error && (
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col col-6">
<div className="form-group">
<label className="form-label">Service Type</label>
<select
className="form-control"
value={formData.serviceType}
onChange={(e) => handleChange('serviceType', e.target.value)}
required
>
{SERVICE_TYPES.map(type => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">New Status</label>
<select
className="form-control"
value={formData.newStatus}
onChange={(e) => handleChange('newStatus', e.target.value)}
required
>
{STATUS_OPTIONS.map(status => (
<option key={status} value={status}>{status}</option>
))}
</select>
</div>
<div className="form-group">
<label className="form-label">New Response Level</label>
<select
className="form-control"
value={formData.newResponseLevel}
onChange={(e) => handleChange('newResponseLevel', e.target.value)}
>
<option value="">Select Response Level</option>
{RESPONSE_LEVELS.map(level => (
<option key={level} value={level}>{level}</option>
))}
</select>
</div>
<div className="form-group">
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={formData.isComment}
onChange={(e) => handleChange('isComment', e.target.checked)}
/>
Nur Kommentar (keine Arbeitszeit)
</label>
</div>
</div>
<div className="col col-6">
<div className="form-group">
<label className="form-label">Total Time (Minuten)</label>
<input
type="number"
className="form-control"
min="0"
step="15"
value={formData.totalTime}
onChange={(e) => handleChange('totalTime', e.target.value)}
disabled={formData.isComment}
placeholder="0"
/>
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
{autoCalculate && formData.startTime && formData.endTime
? '✓ Automatisch berechnet'
: 'Manuell eingeben'}
</small>
</div>
<div className="form-group">
<label className="form-label">Start Date</label>
<input
type="text"
className="form-control"
placeholder="dd.mm.yyyy"
value={formData.startDate}
onChange={(e) => handleChange('startDate', e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">End Date</label>
<input
type="text"
className="form-control"
placeholder="dd.mm.yyyy"
value={formData.endDate}
onChange={(e) => handleChange('endDate', e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">Start Time</label>
<input
type="text"
className="form-control"
placeholder="hhmm"
value={formData.startTime}
onChange={(e) => handleChange('startTime', e.target.value)}
maxLength="4"
/>
</div>
<div className="form-group">
<label className="form-label">End Time</label>
<input
type="text"
className="form-control"
placeholder="hhmm"
value={formData.endTime}
onChange={(e) => handleChange('endTime', e.target.value)}
maxLength="4"
/>
</div>
</div>
</div>
<div className="form-group">
<label className="form-label">Action Details</label>
<textarea
className="form-control"
rows={5}
placeholder="Beschreibe die durchgeführten Arbeiten..."
value={formData.details}
onChange={(e) => handleChange('details', e.target.value)}
required
/>
</div>
<div className="text-center mt-2">
<button
type="submit"
className="btn btn-dark"
disabled={loading}
>
{loading ? 'Creating...' : 'CREATE NOW'}
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -1,44 +1,34 @@
import { FaUserPlus } from 'react-icons/fa6'
// Diese Liste sollte aus der Datenbank kommen
const EDITORS = [
{ id: 'CHLE', name: 'Christian Lehmann' },
{ id: 'DIBR', name: 'Dietmar Bruckauf' },
{ id: 'DOAR', name: 'Dominik Armata' },
{ id: 'GRVO', name: 'Gregor Vowinkel' },
{ id: 'HADW', name: 'Hasan Dwiko' },
{ id: 'JEDI', name: 'Jessica Diaz' },
{ id: 'KNSO', name: 'Kenso Grimm' },
{ id: 'LUPL', name: 'Lukas Placzek' },
{ id: 'NIKI', name: 'Nikita Gaidach' },
{ id: 'MARK', name: 'Marco Kobza' },
{ id: 'MABA', name: 'Markus Bauer' },
{ id: 'MATS', name: 'Maksim Tschetschjotkin' },
{ id: 'PASI', name: 'Pascal Siegfried' },
{ id: 'NICT', name: 'Nico Stegmann' },
{ id: 'MEQU', name: 'Melissa Quednau' },
{ id: 'SASC', name: 'Saskia Schmahl' },
{ id: 'CHPA', name: 'Christin Paulus' },
{ id: 'SOSC', name: 'Sonja Schulze' },
{ id: 'WAWA', name: 'Walter Wawer' },
{ id: 'YAFO', name: 'Yannick Föller' },
{ id: 'TODE', name: 'Tobias Decker' }
]
import { useEmployees } from '../hooks/useEmployees'
export default function EditorDropdown({ value, onChange }) {
const { employees } = useEmployees()
// Finde den zugewiesenen Mitarbeiter anhand der userId
const assignedEmployee = employees.find(emp => emp.userId === value)
// Zeige Kürzel wenn zugewiesen, sonst Icon
const displayValue = assignedEmployee?.shortcode || <FaUserPlus size={20} />
return (
<div className="dropdown">
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
{value || <FaUserPlus size={20} />}
{displayValue}
</button>
<div className="dropdown-content">
{EDITORS.map(editor => (
<span
className="dropdown-item"
onClick={() => onChange('')}
>
<em>(Unassigned)</em>
</span>
{employees.map(employee => (
<span
key={editor.id}
key={employee.$id}
className="dropdown-item"
onClick={() => onChange(editor.id)}
onClick={() => onChange(employee.userId)}
>
{editor.name}
{employee.displayName}{employee.shortcode ? ` (${employee.shortcode})` : ''}
</span>
))}
</div>

View File

@@ -0,0 +1,245 @@
import { useRef, useEffect } from 'react';
const LetterGlitch = ({
glitchColors = ['#2b4539', '#61dca3', '#61b3dc'],
className = '',
glitchSpeed = 50,
centerVignette = false,
outerVignette = true,
smooth = true,
characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$&*()-_+=/[]{};:<>.,0123456789'
}) => {
const canvasRef = useRef(null);
const animationRef = useRef(null);
const letters = useRef([]);
const grid = useRef({ columns: 0, rows: 0 });
const context = useRef(null);
const lastGlitchTime = useRef(Date.now());
const lettersAndSymbols = Array.from(characters);
const fontSize = 16;
const charWidth = 10;
const charHeight = 20;
const getRandomChar = () => {
return lettersAndSymbols[Math.floor(Math.random() * lettersAndSymbols.length)];
};
const getRandomColor = () => {
return glitchColors[Math.floor(Math.random() * glitchColors.length)];
};
const hexToRgb = hex => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
}
: null;
};
const interpolateColor = (start, end, factor) => {
const result = {
r: Math.round(start.r + (end.r - start.r) * factor),
g: Math.round(start.g + (end.g - start.g) * factor),
b: Math.round(start.b + (end.b - start.b) * factor)
};
return `rgb(${result.r}, ${result.g}, ${result.b})`;
};
const calculateGrid = (width, height) => {
const columns = Math.ceil(width / charWidth);
const rows = Math.ceil(height / charHeight);
return { columns, rows };
};
const initializeLetters = (columns, rows) => {
grid.current = { columns, rows };
const totalLetters = columns * rows;
letters.current = Array.from({ length: totalLetters }, () => ({
char: getRandomChar(),
color: getRandomColor(),
targetColor: getRandomColor(),
colorProgress: 1
}));
};
const resizeCanvas = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const parent = canvas.parentElement;
if (!parent) return;
const dpr = window.devicePixelRatio || 1;
const rect = parent.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
if (context.current) {
context.current.setTransform(dpr, 0, 0, dpr, 0, 0);
}
const { columns, rows } = calculateGrid(rect.width, rect.height);
initializeLetters(columns, rows);
drawLetters();
};
const drawLetters = () => {
if (!context.current || letters.current.length === 0) return;
const ctx = context.current;
const { width, height } = canvasRef.current.getBoundingClientRect();
ctx.clearRect(0, 0, width, height);
ctx.font = `${fontSize}px monospace`;
ctx.textBaseline = 'top';
letters.current.forEach((letter, index) => {
const x = (index % grid.current.columns) * charWidth;
const y = Math.floor(index / grid.current.columns) * charHeight;
ctx.fillStyle = letter.color;
ctx.fillText(letter.char, x, y);
});
};
const updateLetters = () => {
if (!letters.current || letters.current.length === 0) return;
const updateCount = Math.max(1, Math.floor(letters.current.length * 0.05));
for (let i = 0; i < updateCount; i++) {
const index = Math.floor(Math.random() * letters.current.length);
if (!letters.current[index]) continue;
letters.current[index].char = getRandomChar();
letters.current[index].targetColor = getRandomColor();
if (!smooth) {
letters.current[index].color = letters.current[index].targetColor;
letters.current[index].colorProgress = 1;
} else {
letters.current[index].colorProgress = 0;
}
}
};
const handleSmoothTransitions = () => {
let needsRedraw = false;
letters.current.forEach(letter => {
if (letter.colorProgress < 1) {
letter.colorProgress += 0.05;
if (letter.colorProgress > 1) letter.colorProgress = 1;
const startRgb = hexToRgb(letter.color);
const endRgb = hexToRgb(letter.targetColor);
if (startRgb && endRgb) {
letter.color = interpolateColor(startRgb, endRgb, letter.colorProgress);
needsRedraw = true;
}
}
});
if (needsRedraw) {
drawLetters();
}
};
const animate = () => {
const now = Date.now();
if (now - lastGlitchTime.current >= glitchSpeed) {
updateLetters();
drawLetters();
lastGlitchTime.current = now;
}
if (smooth) {
handleSmoothTransitions();
}
animationRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
context.current = canvas.getContext('2d');
resizeCanvas();
animate();
let resizeTimeout;
const handleResize = () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
cancelAnimationFrame(animationRef.current);
resizeCanvas();
animate();
}, 100);
};
window.addEventListener('resize', handleResize);
return () => {
cancelAnimationFrame(animationRef.current);
window.removeEventListener('resize', handleResize);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [glitchSpeed, smooth]);
const containerStyle = {
position: 'relative',
width: '100%',
height: '100%',
backgroundColor: '#000000',
overflow: 'hidden'
};
const canvasStyle = {
display: 'block',
width: '100%',
height: '100%'
};
const outerVignetteStyle = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
background: 'radial-gradient(circle, rgba(0,0,0,0) 60%, rgba(0,0,0,1) 100%)'
};
const centerVignetteStyle = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
background: 'radial-gradient(circle, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 60%)'
};
return (
<div style={containerStyle} className={className}>
<canvas ref={canvasRef} style={canvasStyle} />
{outerVignette && <div style={outerVignetteStyle}></div>}
{centerVignette && <div style={centerVignetteStyle}></div>}
</div>
);
};
export default LetterGlitch;

View File

@@ -0,0 +1,138 @@
/* Modern Sidebar Styles */
.modern-sidebar-desktop {
height: 100vh;
padding: 1rem;
display: none;
flex-direction: column;
background: var(--sidebar-bg, #f5f5f5);
border-right: 1px solid var(--border-color, #e0e0e0);
flex-shrink: 0;
overflow: hidden;
transition: width 0.3s ease;
}
@media (min-width: 768px) {
.modern-sidebar-desktop {
display: flex;
}
}
/* Mobile Sidebar */
.modern-sidebar-mobile-header {
height: 60px;
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--sidebar-bg, #f5f5f5);
border-bottom: 1px solid var(--border-color, #e0e0e0);
width: 100%;
}
@media (min-width: 768px) {
.modern-sidebar-mobile-header {
display: none;
}
}
.modern-sidebar-mobile-toggle {
display: flex;
justify-content: flex-end;
width: 100%;
cursor: pointer;
color: var(--text-color, #333);
}
.modern-sidebar-mobile-toggle svg {
width: 24px;
height: 24px;
}
.modern-sidebar-mobile-menu {
position: fixed;
height: 100vh;
width: 100%;
inset: 0;
background: var(--sidebar-bg, #fff);
padding: 2.5rem;
z-index: 100;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.modern-sidebar-mobile-close {
position: absolute;
right: 2.5rem;
top: 2.5rem;
z-index: 50;
cursor: pointer;
color: var(--text-color, #333);
}
.modern-sidebar-mobile-close svg {
width: 24px;
height: 24px;
}
/* Sidebar Link */
.modern-sidebar-link {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.5rem;
padding: 0.75rem 0.5rem;
color: var(--text-color, #333);
text-decoration: none;
border-radius: 6px;
transition: all 0.15s ease;
}
.modern-sidebar-link:hover {
background: var(--hover-bg, rgba(0, 0, 0, 0.05));
transform: translateX(2px);
}
.modern-sidebar-link-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.modern-sidebar-link-icon svg {
width: 20px;
height: 20px;
}
.modern-sidebar-link-label {
font-size: 0.875rem;
white-space: pre;
margin: 0;
padding: 0;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.modern-sidebar-desktop,
.modern-sidebar-mobile-header,
.modern-sidebar-mobile-menu {
background: var(--dark-sidebar-bg, #1a1a1a);
border-color: var(--dark-border-color, #333);
}
.modern-sidebar-link {
color: var(--dark-text-color, #e0e0e0);
}
.modern-sidebar-link:hover {
background: var(--dark-hover-bg, rgba(255, 255, 255, 0.1));
}
.modern-sidebar-mobile-toggle,
.modern-sidebar-mobile-close {
color: var(--dark-text-color, #e0e0e0);
}
}

View File

@@ -0,0 +1,116 @@
import { useState, createContext, useContext } from 'react'
import { motion, AnimatePresence } from 'motion/react'
import { IconMenu2, IconX } from '@tabler/icons-react'
import { cn } from '../lib/utils'
import './ModernSidebar.css'
const SidebarContext = createContext(undefined)
export const useSidebar = () => {
const context = useContext(SidebarContext)
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider')
}
return context
}
export const SidebarProvider = ({ children, open: openProp, setOpen: setOpenProp, animate = true }) => {
const [openState, setOpenState] = useState(false)
const open = openProp !== undefined ? openProp : openState
const setOpen = setOpenProp !== undefined ? setOpenProp : setOpenState
return (
<SidebarContext.Provider value={{ open, setOpen, animate }}>
{children}
</SidebarContext.Provider>
)
}
export const Sidebar = ({ children, open, setOpen, animate }) => {
return (
<SidebarProvider open={open} setOpen={setOpen} animate={animate}>
{children}
</SidebarProvider>
)
}
export const SidebarBody = ({ className, children, ...props }) => {
return (
<>
<DesktopSidebar className={className} {...props}>{children}</DesktopSidebar>
<MobileSidebar className={className}>{children}</MobileSidebar>
</>
)
}
export const DesktopSidebar = ({ className, children, ...props }) => {
const { open, setOpen, animate } = useSidebar()
return (
<motion.div
className={cn('modern-sidebar-desktop', className)}
animate={{
width: animate ? (open ? '300px' : '60px') : '300px',
}}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
{...props}
>
{children}
</motion.div>
)
}
export const MobileSidebar = ({ className, children }) => {
const { open, setOpen } = useSidebar()
return (
<>
<div className="modern-sidebar-mobile-header">
<div className="modern-sidebar-mobile-toggle">
<IconMenu2 onClick={() => setOpen(!open)} />
</div>
<AnimatePresence>
{open && (
<motion.div
initial={{ x: '-100%', opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: '-100%', opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
className={cn('modern-sidebar-mobile-menu', className)}
>
<div className="modern-sidebar-mobile-close" onClick={() => setOpen(!open)}>
<IconX />
</div>
{children}
</motion.div>
)}
</AnimatePresence>
</div>
</>
)
}
export const SidebarLink = ({ link, className, ...props }) => {
const { open, animate } = useSidebar()
return (
<a
href={link.href}
className={cn('modern-sidebar-link', className)}
{...props}
>
<span className="modern-sidebar-link-icon">{link.icon}</span>
<motion.span
animate={{
display: animate ? (open ? 'inline-block' : 'none') : 'inline-block',
opacity: animate ? (open ? 1 : 0) : 1,
}}
className="modern-sidebar-link-label"
>
{link.label}
</motion.span>
</a>
)
}

View File

@@ -1,49 +1,174 @@
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { FaUser } from 'react-icons/fa'
import { motion } from 'motion/react'
import {
IconTicket,
IconDeviceDesktop,
IconChartBar,
IconCalendar,
IconFolders,
IconFileText,
IconBook,
IconSettings,
IconLogout
} from '@tabler/icons-react'
import { Sidebar, SidebarBody, SidebarLink } from './ModernSidebar'
export default function Navbar() {
const { user, logout } = useAuth()
const { user, logout, isAdmin } = useAuth()
const navigate = useNavigate()
const [open, setOpen] = useState(false)
const handleLogout = async () => {
await logout()
navigate('/login')
}
return (
<nav className="navbar">
<div className="navbar-brand">
<span>NetWEB</span>
<img src="/logo.png" alt="" height="18" width="18" />
<span>Systems</span>
</div>
<ul className="navbar-nav">
<li><Link to="/tickets" className="nav-link">Tickets</Link></li>
<li><Link to="/assets" className="nav-link">Assets</Link></li>
<li><Link to="/dashboard" className="nav-link">Dashboard</Link></li>
<li><Link to="/planboard" className="nav-link">Planboard</Link></li>
<li><Link to="/projects" className="nav-link">Projects</Link></li>
<li><Link to="/reports" className="nav-link">Reports</Link></li>
<li><Link to="/docs" className="nav-link">Docs</Link></li>
</ul>
const links = [
{
label: 'Tickets',
href: '/tickets',
icon: <IconTicket className="icon" />,
},
{
label: 'Assets',
href: '/assets',
icon: <IconDeviceDesktop className="icon" />,
},
{
label: 'Dashboard',
href: '/dashboard',
icon: <IconChartBar className="icon" />,
},
{
label: 'Planboard',
href: '/planboard',
icon: <IconCalendar className="icon" />,
},
{
label: 'Projects',
href: '/projects',
icon: <IconFolders className="icon" />,
},
{
label: 'Reports',
href: '/reports',
icon: <IconFileText className="icon" />,
},
{
label: 'Docs',
href: '/docs',
icon: <IconBook className="icon" />,
},
]
<div className="nav-right">
{user ? (
<button
onClick={handleLogout}
className="nav-link"
style={{ background: 'none', border: 'none', cursor: 'pointer' }}
>
<FaUser /> Logout, {user.name || user.email}
</button>
) : (
<Link to="/login" className="nav-link">
<FaUser /> Login
</Link>
)}
</div>
</nav>
// Admin link nur wenn isAdmin
if (isAdmin) {
links.push({
label: 'Admin',
href: '/admin',
icon: <IconSettings className="icon" />,
})
}
return (
<Sidebar open={open} setOpen={setOpen}>
<SidebarBody className="justify-between gap-10">
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowX: 'hidden', overflowY: 'auto' }}>
<Logo />
<div style={{ marginTop: '2rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{links.map((link, idx) => (
<SidebarLink key={idx} link={link} />
))}
</div>
</div>
<div>
{user ? (
<>
<SidebarLink
link={{
label: user.name || user.email,
href: '#',
icon: (
<div style={{
height: '28px',
width: '28px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '12px',
fontWeight: 'bold',
flexShrink: 0
}}>
{(user.name || user.email).charAt(0).toUpperCase()}
</div>
),
}}
/>
<div onClick={handleLogout} style={{ cursor: 'pointer', marginTop: '0.5rem' }}>
<SidebarLink
link={{
label: 'Logout',
href: '#',
icon: <IconLogout className="icon" />,
}}
/>
</div>
</>
) : (
<SidebarLink
link={{
label: 'Login',
href: '/login',
icon: <IconLogout className="icon" />,
}}
/>
)}
</div>
</SidebarBody>
</Sidebar>
)
}
const Logo = () => {
return (
<Link
to="/"
style={{
position: 'relative',
zIndex: 20,
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.25rem 0',
fontSize: '0.875rem',
fontWeight: 'normal',
color: 'var(--text-color, #333)',
textDecoration: 'none'
}}
>
<div style={{
height: '20px',
width: '24px',
flexShrink: 0,
borderRadius: '4px 2px 4px 2px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}} />
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
style={{
fontWeight: 500,
whiteSpace: 'pre',
color: 'var(--text-color, #333)'
}}
>
Webklar
</motion.span>
</Link>
)
}

View File

@@ -0,0 +1,7 @@
.pixel-blast-container {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}

View File

@@ -0,0 +1,674 @@
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { EffectComposer, EffectPass, RenderPass, Effect } from 'postprocessing';
import './PixelBlast.css';
const createTouchTexture = () => {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('2D context not available');
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.Texture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.generateMipmaps = false;
const trail = [];
let last = null;
const maxAge = 64;
let radius = 0.1 * size;
const speed = 1 / maxAge;
const clear = () => {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
const drawPoint = p => {
const pos = { x: p.x * size, y: (1 - p.y) * size };
let intensity = 1;
const easeOutSine = t => Math.sin((t * Math.PI) / 2);
const easeOutQuad = t => -t * (t - 2);
if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3));
else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0;
intensity *= p.force;
const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`;
const offset = size * 5;
ctx.shadowOffsetX = offset;
ctx.shadowOffsetY = offset;
ctx.shadowBlur = radius;
ctx.shadowColor = `rgba(${color},${0.22 * intensity})`;
ctx.beginPath();
ctx.fillStyle = 'rgba(255,0,0,1)';
ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2);
ctx.fill();
};
const addTouch = norm => {
let force = 0;
let vx = 0;
let vy = 0;
if (last) {
const dx = norm.x - last.x;
const dy = norm.y - last.y;
if (dx === 0 && dy === 0) return;
const dd = dx * dx + dy * dy;
const d = Math.sqrt(dd);
vx = dx / (d || 1);
vy = dy / (d || 1);
force = Math.min(dd * 10000, 1);
}
last = { x: norm.x, y: norm.y };
trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy });
};
const update = () => {
clear();
for (let i = trail.length - 1; i >= 0; i--) {
const point = trail[i];
const f = point.force * speed * (1 - point.age / maxAge);
point.x += point.vx * f;
point.y += point.vy * f;
point.age++;
if (point.age > maxAge) trail.splice(i, 1);
}
for (let i = 0; i < trail.length; i++) drawPoint(trail[i]);
texture.needsUpdate = true;
};
return {
canvas,
texture,
addTouch,
update,
set radiusScale(v) {
radius = 0.1 * size * v;
},
get radiusScale() {
return radius / (0.1 * size);
},
size
};
};
const createLiquidEffect = (texture, opts) => {
const fragment = `
uniform sampler2D uTexture;
uniform float uStrength;
uniform float uTime;
uniform float uFreq;
void mainUv(inout vec2 uv) {
vec4 tex = texture2D(uTexture, uv);
float vx = tex.r * 2.0 - 1.0;
float vy = tex.g * 2.0 - 1.0;
float intensity = tex.b;
float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);
float amt = uStrength * intensity * wave;
uv += vec2(vx, vy) * amt;
}
`;
return new Effect('LiquidEffect', fragment, {
uniforms: new Map([
['uTexture', new THREE.Uniform(texture)],
['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],
['uTime', new THREE.Uniform(0)],
['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)]
])
});
};
const SHAPE_MAP = {
square: 0,
circle: 1,
triangle: 2,
diamond: 3
};
const VERTEX_SRC = `
void main() {
gl_Position = vec4(position, 1.0);
}
`;
const FRAGMENT_SRC = `
precision highp float;
uniform vec3 uColor;
uniform vec2 uResolution;
uniform float uTime;
uniform float uPixelSize;
uniform float uScale;
uniform float uDensity;
uniform float uPixelJitter;
uniform int uEnableRipples;
uniform float uRippleSpeed;
uniform float uRippleThickness;
uniform float uRippleIntensity;
uniform float uEdgeFade;
uniform int uShapeType;
const int SHAPE_SQUARE = 0;
const int SHAPE_CIRCLE = 1;
const int SHAPE_TRIANGLE = 2;
const int SHAPE_DIAMOND = 3;
const int MAX_CLICKS = 10;
uniform vec2 uClickPos [MAX_CLICKS];
uniform float uClickTimes[MAX_CLICKS];
out vec4 fragColor;
float Bayer2(vec2 a) {
a = floor(a);
return fract(a.x / 2. + a.y * a.y * .75);
}
#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))
#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))
#define FBM_OCTAVES 5
#define FBM_LACUNARITY 1.25
#define FBM_GAIN 1.0
float hash11(float n){ return fract(sin(n)*43758.5453); }
float vnoise(vec3 p){
vec3 ip = floor(p);
vec3 fp = fract(p);
float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));
float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));
float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));
float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));
float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));
float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));
float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));
float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));
vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);
float x00 = mix(n000, n100, w.x);
float x10 = mix(n010, n110, w.x);
float x01 = mix(n001, n101, w.x);
float x11 = mix(n011, n111, w.x);
float y0 = mix(x00, x10, w.y);
float y1 = mix(x01, x11, w.y);
return mix(y0, y1, w.z) * 2.0 - 1.0;
}
float fbm2(vec2 uv, float t){
vec3 p = vec3(uv * uScale, t);
float amp = 1.0;
float freq = 1.0;
float sum = 1.0;
for (int i = 0; i < FBM_OCTAVES; ++i){
sum += amp * vnoise(p * freq);
freq *= FBM_LACUNARITY;
amp *= FBM_GAIN;
}
return sum * 0.5 + 0.5;
}
float maskCircle(vec2 p, float cov){
float r = sqrt(cov) * .25;
float d = length(p - 0.5) - r;
float aa = 0.5 * fwidth(d);
return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));
}
float maskTriangle(vec2 p, vec2 id, float cov){
bool flip = mod(id.x + id.y, 2.0) > 0.5;
if (flip) p.x = 1.0 - p.x;
float r = sqrt(cov);
float d = p.y - r*(1.0 - p.x);
float aa = fwidth(d);
return cov * clamp(0.5 - d/aa, 0.0, 1.0);
}
float maskDiamond(vec2 p, float cov){
float r = sqrt(cov) * 0.564;
return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);
}
void main(){
float pixelSize = uPixelSize;
vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;
float aspectRatio = uResolution.x / uResolution.y;
vec2 pixelId = floor(fragCoord / pixelSize);
vec2 pixelUV = fract(fragCoord / pixelSize);
float cellPixelSize = 8.0 * pixelSize;
vec2 cellId = floor(fragCoord / cellPixelSize);
vec2 cellCoord = cellId * cellPixelSize;
vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);
float base = fbm2(uv, uTime * 0.05);
base = base * 0.5 - 0.65;
float feed = base + (uDensity - 0.5) * 0.3;
float speed = uRippleSpeed;
float thickness = uRippleThickness;
const float dampT = 1.0;
const float dampR = 10.0;
if (uEnableRipples == 1) {
for (int i = 0; i < MAX_CLICKS; ++i){
vec2 pos = uClickPos[i];
if (pos.x < 0.0) continue;
float cellPixelSize = 8.0 * pixelSize;
vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);
float t = max(uTime - uClickTimes[i], 0.0);
float r = distance(uv, cuv);
float waveR = speed * t;
float ring = exp(-pow((r - waveR) / thickness, 2.0));
float atten = exp(-dampT * t) * exp(-dampR * r);
feed = max(feed, ring * atten * uRippleIntensity);
}
}
float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
float bw = step(0.5, feed + bayer);
float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);
float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;
float coverage = bw * jitterScale;
float M;
if (uShapeType == SHAPE_CIRCLE) M = maskCircle (pixelUV, coverage);
else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);
else if (uShapeType == SHAPE_DIAMOND) M = maskDiamond(pixelUV, coverage);
else M = coverage;
if (uEdgeFade > 0.0) {
vec2 norm = gl_FragCoord.xy / uResolution;
float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));
float fade = smoothstep(0.0, uEdgeFade, edge);
M *= fade;
}
vec3 color = uColor;
fragColor = vec4(color, M);
}
`;
const MAX_CLICKS = 10;
const PixelBlast = ({
variant = 'square',
pixelSize = 3,
color = '#10b981',
className,
style,
antialias = true,
patternScale = 2,
patternDensity = 1,
liquid = false,
liquidStrength = 0.1,
liquidRadius = 1,
pixelSizeJitter = 0,
enableRipples = true,
rippleIntensityScale = 1,
rippleThickness = 0.1,
rippleSpeed = 0.3,
liquidWobbleSpeed = 4.5,
autoPauseOffscreen = true,
speed = 0.5,
transparent = true,
edgeFade = 0.5,
noiseAmount = 0
}) => {
const containerRef = useRef(null);
const visibilityRef = useRef({ visible: true });
const speedRef = useRef(speed);
const threeRef = useRef(null);
const prevConfigRef = useRef(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
speedRef.current = speed;
const needsReinitKeys = ['antialias', 'liquid', 'noiseAmount'];
const cfg = { antialias, liquid, noiseAmount };
let mustReinit = false;
if (!threeRef.current) mustReinit = true;
else if (prevConfigRef.current) {
for (const k of needsReinitKeys)
if (prevConfigRef.current[k] !== cfg[k]) {
mustReinit = true;
break;
}
}
if (mustReinit) {
if (threeRef.current) {
const t = threeRef.current;
t.resizeObserver?.disconnect();
cancelAnimationFrame(t.raf);
t.quad?.geometry.dispose();
t.material.dispose();
t.composer?.dispose();
t.renderer.dispose();
if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);
threeRef.current = null;
}
const canvas = document.createElement('canvas');
const renderer = new THREE.WebGLRenderer({
canvas,
antialias,
alpha: true,
powerPreference: 'high-performance'
});
renderer.domElement.style.width = '100%';
renderer.domElement.style.height = '100%';
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
container.appendChild(renderer.domElement);
if (transparent) renderer.setClearAlpha(0);
else renderer.setClearColor(0x000000, 1);
const uniforms = {
uResolution: { value: new THREE.Vector2(0, 0) },
uTime: { value: 0 },
uColor: { value: new THREE.Color(color) },
uClickPos: {
value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1))
},
uClickTimes: { value: new Float32Array(MAX_CLICKS) },
uShapeType: { value: SHAPE_MAP[variant] ?? 0 },
uPixelSize: { value: pixelSize * renderer.getPixelRatio() },
uScale: { value: patternScale },
uDensity: { value: patternDensity },
uPixelJitter: { value: pixelSizeJitter },
uEnableRipples: { value: enableRipples ? 1 : 0 },
uRippleSpeed: { value: rippleSpeed },
uRippleThickness: { value: rippleThickness },
uRippleIntensity: { value: rippleIntensityScale },
uEdgeFade: { value: edgeFade }
};
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const material = new THREE.ShaderMaterial({
vertexShader: VERTEX_SRC,
fragmentShader: FRAGMENT_SRC,
uniforms,
transparent: true,
depthTest: false,
depthWrite: false,
glslVersion: THREE.GLSL3
});
const quadGeom = new THREE.PlaneGeometry(2, 2);
const quad = new THREE.Mesh(quadGeom, material);
scene.add(quad);
const clock = new THREE.Clock();
const setSize = () => {
const w = container.clientWidth || 1;
const h = container.clientHeight || 1;
renderer.setSize(w, h, false);
uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height);
if (threeRef.current?.composer)
threeRef.current.composer.setSize(renderer.domElement.width, renderer.domElement.height);
uniforms.uPixelSize.value = pixelSize * renderer.getPixelRatio();
};
setSize();
const ro = new ResizeObserver(setSize);
ro.observe(container);
const randomFloat = () => {
if (typeof window !== 'undefined' && window.crypto?.getRandomValues) {
const u32 = new Uint32Array(1);
window.crypto.getRandomValues(u32);
return u32[0] / 0xffffffff;
}
return Math.random();
};
const timeOffset = randomFloat() * 1000;
let composer;
let touch;
let liquidEffect;
if (liquid) {
touch = createTouchTexture();
touch.radiusScale = liquidRadius;
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
liquidEffect = createLiquidEffect(touch.texture, {
strength: liquidStrength,
freq: liquidWobbleSpeed
});
const effectPass = new EffectPass(camera, liquidEffect);
effectPass.renderToScreen = true;
composer.addPass(renderPass);
composer.addPass(effectPass);
}
if (noiseAmount > 0) {
if (!composer) {
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
}
const noiseEffect = new Effect(
'NoiseEffect',
`uniform float uTime; uniform float uAmount; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);} void mainUv(inout vec2 uv){} void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor){ float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0)); float g=(n-0.5)*uAmount; outputColor=inputColor+vec4(vec3(g),0.0);} `,
{
uniforms: new Map([
['uTime', new THREE.Uniform(0)],
['uAmount', new THREE.Uniform(noiseAmount)]
])
}
);
const noisePass = new EffectPass(camera, noiseEffect);
noisePass.renderToScreen = true;
if (composer && composer.passes.length > 0) composer.passes.forEach(p => (p.renderToScreen = false));
composer.addPass(noisePass);
}
if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height);
const mapToPixels = e => {
const rect = renderer.domElement.getBoundingClientRect();
const scaleX = renderer.domElement.width / rect.width;
const scaleY = renderer.domElement.height / rect.height;
const fx = (e.clientX - rect.left) * scaleX;
const fy = (rect.height - (e.clientY - rect.top)) * scaleY;
return {
fx,
fy,
w: renderer.domElement.width,
h: renderer.domElement.height
};
};
const onPointerDown = e => {
const { fx, fy } = mapToPixels(e);
const ix = threeRef.current?.clickIx ?? 0;
uniforms.uClickPos.value[ix].set(fx, fy);
uniforms.uClickTimes.value[ix] = uniforms.uTime.value;
if (threeRef.current) threeRef.current.clickIx = (ix + 1) % MAX_CLICKS;
};
const onPointerMove = e => {
if (!touch) return;
const { fx, fy, w, h } = mapToPixels(e);
touch.addTouch({ x: fx / w, y: fy / h });
};
renderer.domElement.addEventListener('pointerdown', onPointerDown, {
passive: true
});
renderer.domElement.addEventListener('pointermove', onPointerMove, {
passive: true
});
let raf = 0;
const animate = () => {
if (autoPauseOffscreen && !visibilityRef.current.visible) {
raf = requestAnimationFrame(animate);
return;
}
uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.current;
if (liquidEffect) liquidEffect.uniforms.get('uTime').value = uniforms.uTime.value;
if (composer) {
if (touch) touch.update();
composer.passes.forEach(p => {
const effs = p.effects;
if (effs)
effs.forEach(eff => {
const u = eff.uniforms?.get('uTime');
if (u) u.value = uniforms.uTime.value;
});
});
composer.render();
} else renderer.render(scene, camera);
raf = requestAnimationFrame(animate);
};
raf = requestAnimationFrame(animate);
threeRef.current = {
renderer,
scene,
camera,
material,
clock,
clickIx: 0,
uniforms,
resizeObserver: ro,
raf,
quad,
timeOffset,
composer,
touch,
liquidEffect
};
} else {
const t = threeRef.current;
t.uniforms.uShapeType.value = SHAPE_MAP[variant] ?? 0;
t.uniforms.uPixelSize.value = pixelSize * t.renderer.getPixelRatio();
t.uniforms.uColor.value.set(color);
t.uniforms.uScale.value = patternScale;
t.uniforms.uDensity.value = patternDensity;
t.uniforms.uPixelJitter.value = pixelSizeJitter;
t.uniforms.uEnableRipples.value = enableRipples ? 1 : 0;
t.uniforms.uRippleIntensity.value = rippleIntensityScale;
t.uniforms.uRippleThickness.value = rippleThickness;
t.uniforms.uRippleSpeed.value = rippleSpeed;
t.uniforms.uEdgeFade.value = edgeFade;
if (transparent) t.renderer.setClearAlpha(0);
else t.renderer.setClearColor(0x000000, 1);
if (t.liquidEffect) {
const uStrength = t.liquidEffect.uniforms.get('uStrength');
if (uStrength) uStrength.value = liquidStrength;
const uFreq = t.liquidEffect.uniforms.get('uFreq');
if (uFreq) uFreq.value = liquidWobbleSpeed;
}
if (t.touch) t.touch.radiusScale = liquidRadius;
}
prevConfigRef.current = cfg;
return () => {
if (threeRef.current && mustReinit) return;
if (!threeRef.current) return;
const t = threeRef.current;
t.resizeObserver?.disconnect();
cancelAnimationFrame(t.raf);
t.quad?.geometry.dispose();
t.material.dispose();
t.composer?.dispose();
t.renderer.dispose();
if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);
threeRef.current = null;
};
}, [
antialias,
liquid,
noiseAmount,
pixelSize,
patternScale,
patternDensity,
enableRipples,
rippleIntensityScale,
rippleThickness,
rippleSpeed,
pixelSizeJitter,
edgeFade,
transparent,
liquidStrength,
liquidRadius,
liquidWobbleSpeed,
autoPauseOffscreen,
variant,
color,
speed
]);
return (
<div
ref={containerRef}
className={`pixel-blast-container ${className ?? ''}`}
style={style}
aria-label="PixelBlast interactive background"
/>
);
};
export default PixelBlast;

View File

@@ -0,0 +1,143 @@
import { FaTimes } from 'react-icons/fa'
import { format } from 'date-fns'
export default function StatusHistoryModal({ isOpen, onClose, worksheets, ticket }) {
if (!isOpen) return null
// Extrahiere Status-Änderungen aus Worksheets
const statusHistory = worksheets
.filter(ws => ws.oldStatus && ws.newStatus && ws.oldStatus !== ws.newStatus)
.map(ws => ({
date: ws.startDate,
time: ws.startTime,
from: ws.oldStatus,
to: ws.newStatus,
employee: ws.employeeName || ws.employeeShort || 'Unknown',
details: ws.details,
wsid: ws.wsid
}))
.sort((a, b) => {
// Sortiere nach Datum und Zeit (älteste zuerst)
const dateA = `${a.date} ${a.time || '0000'}`
const dateB = `${b.date} ${b.time || '0000'}`
return dateA.localeCompare(dateB)
})
// Füge aktuellen Status hinzu
const currentStatus = ticket?.status || 'Open'
return (
<div className="overlay">
<span className="overlay-close" onClick={onClose}>
<FaTimes />
</span>
<div className="overlay-content">
<h2 className="mb-2">Status History - WOID {ticket?.woid || ticket?.$id}</h2>
<div style={{ marginBottom: '24px' }}>
<div style={{
background: 'rgba(45, 55, 72, 0.95)',
borderRadius: '8px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
marginBottom: '16px'
}}>
<strong style={{ color: 'var(--green-primary)' }}>Current Status:</strong>
<span style={{
marginLeft: '12px',
padding: '4px 12px',
borderRadius: '4px',
background: 'rgba(16, 185, 129, 0.2)',
color: 'var(--dark-text)',
fontWeight: 'bold'
}}>
{currentStatus}
</span>
</div>
</div>
{statusHistory.length === 0 ? (
<div style={{
background: 'rgba(45, 55, 72, 0.95)',
borderRadius: '8px',
padding: '24px',
textAlign: 'center',
color: '#a0aec0'
}}>
No status changes recorded yet.
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{statusHistory.map((entry, index) => (
<div
key={index}
style={{
background: 'rgba(45, 55, 72, 0.95)',
borderRadius: '8px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
borderLeft: '4px solid var(--green-primary)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start', marginBottom: '8px' }}>
<div>
<div style={{ fontSize: '14px', color: '#a0aec0', marginBottom: '4px' }}>
{entry.date} {entry.time ? `${entry.time.substring(0, 2)}:${entry.time.substring(2, 4)}` : ''}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
background: 'rgba(74, 85, 104, 0.5)',
color: 'var(--dark-text)',
fontSize: '12px'
}}>
{entry.from}
</span>
<span style={{ color: 'var(--green-primary)' }}></span>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
background: 'rgba(16, 185, 129, 0.3)',
color: 'var(--dark-text)',
fontSize: '12px',
fontWeight: 'bold'
}}>
{entry.to}
</span>
</div>
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '12px', color: '#a0aec0' }}>by</div>
<div style={{ fontWeight: 'bold', color: 'var(--dark-text)' }}>{entry.employee}</div>
{entry.wsid && (
<div style={{ fontSize: '11px', color: '#718096', marginTop: '4px' }}>
WSID: {entry.wsid}
</div>
)}
</div>
</div>
{entry.details && (
<div style={{
marginTop: '12px',
padding: '12px',
background: 'rgba(31, 41, 55, 0.6)',
borderRadius: '6px',
fontSize: '13px',
color: '#cbd5e0',
borderLeft: '3px solid rgba(16, 185, 129, 0.4)'
}}>
{entry.details}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
)
}

View File

@@ -1,11 +1,16 @@
import { useState } from 'react'
import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear } from 'react-icons/fa6'
import { FaLock, FaLockOpen, FaPlay, FaStop, FaTruck, FaSackDollar, FaUserGear, FaPlus, FaClockRotateLeft } from 'react-icons/fa6'
import { formatDistanceToNow, format } from 'date-fns'
import { de } from 'date-fns/locale'
import StatusDropdown from './StatusDropdown'
import PriorityDropdown from './PriorityDropdown'
import EditorDropdown from './EditorDropdown'
import ResponseDropdown from './ResponseDropdown'
import CreateWorksheetModal from './CreateWorksheetModal'
import StatusHistoryModal from './StatusHistoryModal'
import WorksheetList from './WorksheetList'
import WorksheetStats from './WorksheetStats'
import { useWorksheets } from '../hooks/useWorksheets'
const PRIORITY_CLASSES = {
0: 'priority-none',
@@ -41,6 +46,16 @@ const APPROVAL_ICONS = {
export default function TicketRow({ ticket, onUpdate, onExpand }) {
const [expanded, setExpanded] = useState(false)
const [locked, setLocked] = useState(true)
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
const [showHistoryModal, setShowHistoryModal] = useState(false)
// Worksheets für dieses Ticket laden (nur wenn expanded)
const {
worksheets,
loading: worksheetsLoading,
createWorksheet,
getTotalTime
} = useWorksheets(expanded ? ticket.woid : null)
const createdAt = new Date(ticket.$createdAt || ticket.createdAt)
const elapsed = formatDistanceToNow(createdAt, { locale: de })
@@ -71,13 +86,27 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
setLocked(!locked)
}
const handleCreateWorksheet = async (worksheetData, currentUser) => {
const result = await createWorksheet(worksheetData, currentUser)
// Wenn Status geändert wurde, aktualisiere Work Order
if (result.success && worksheetData.newStatus !== ticket.status) {
await onUpdate(ticket.$id, {
status: worksheetData.newStatus,
responseLevel: worksheetData.newResponseLevel || ticket.responseLevel
})
}
return result
}
const ApprovalIcon = APPROVAL_ICONS[ticket.approvalStatus] || FaUserGear
return (
<>
<tr className="ticket-row">
<td className="ticket-id" rowSpan={2}>
<div>{ticket.woid || ticket.$id?.slice(-5)}</div>
<tr className={`ticket-row ${expanded ? 'ticket-expanded' : 'ticket-collapsed'}`}>
<td className={`ticket-id ${expanded ? 'ticket-id-expanded' : ''}`} rowSpan={2}>
<div><strong>WOID:</strong> {ticket.woid || ticket.$id?.slice(-5)}</div>
<div className="ticket-time">{elapsed}</div>
</td>
<td className="ticket-info" rowSpan={2}>
@@ -150,21 +179,176 @@ export default function TicketRow({ ticket, onUpdate, onExpand }) {
</tr>
{expanded && (
<>
<tr>
<td colSpan={10} className="p-2">
<div className="card">
<div className="card-header">Details - WOID {ticket.woid || ticket.$id}</div>
<div className="card-body">
<p><strong>Beschreibung:</strong></p>
<p>{ticket.details || 'Keine Details vorhanden.'}</p>
<tr className="worksheet-expansion">
<td colSpan={10} className="worksheet-cell">
<div className="card" style={{
borderRadius: '0 0 12px 12px',
marginTop: 0,
border: '1px solid rgba(16, 185, 129, 0.2)',
borderTop: 'none'
}}>
<div className="card-body" style={{ borderRadius: '0 0 12px 12px', padding: '20px' }}>
{/* Bento Box Layout: 2 Spalten */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '20px',
alignItems: 'stretch'
}}>
{/* Linke Spalte: Ticket-Beschreibung (50%) */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '20px',
border: '1px solid rgba(16, 185, 129, 0.2)',
display: 'flex',
flexDirection: 'column',
height: '100%'
}}>
<h5 style={{
color: 'var(--dark-text)',
fontWeight: 'bold',
marginBottom: '16px',
fontSize: '18px',
flex: '0 0 auto'
}}>
📋 Ticket-Beschreibung
</h5>
<p style={{
fontSize: '14px',
lineHeight: '1.8',
color: 'rgba(226, 232, 240, 0.8)',
whiteSpace: 'pre-wrap',
margin: 0,
flex: '1 1 auto',
overflowY: 'auto'
}}>
{ticket.details || 'Keine Details vorhanden.'}
</p>
</div>
{/* Rechte Spalte: Statistics, Buttons (50%) */}
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
height: '100%'
}}>
{/* Button Row: Add Worksheet (100%) + History Icon Button */}
<div style={{
display: 'flex',
gap: '8px',
alignItems: 'stretch'
}}>
{/* Add Worksheet Button - 100% width minus icon button */}
<button
className="btn"
style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white',
border: 'none',
padding: '12px 20px',
borderRadius: '8px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'all 0.2s ease',
flex: 1
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(16, 185, 129, 0.4)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}
onClick={() => setShowCreateWorksheet(true)}
>
<FaPlus style={{ marginRight: '8px' }} /> Add Worksheet
</button>
{/* History Icon Button - klein, grau, nur Icon */}
<button
style={{
background: '#616161',
color: 'white',
border: 'none',
padding: '12px',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '44px',
width: '44px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#757575'
e.currentTarget.style.transform = 'translateY(-2px)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#616161'
e.currentTarget.style.transform = 'translateY(0)'
}}
onClick={() => setShowHistoryModal(true)}
title="Status History"
>
<FaClockRotateLeft size={18} />
</button>
</div>
{/* Statistiken */}
{worksheets.length > 0 && (
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '1 1 auto',
display: 'flex',
flexDirection: 'column',
minHeight: 0
}}>
<WorksheetStats worksheets={worksheets} />
</div>
)}
</div>
</div>
{/* Gesamtarbeitszeit und Worksheet-Liste - 100% Breite unter dem Bento Box */}
<div style={{
marginTop: '20px',
width: '100%'
}}>
<WorksheetList
worksheets={worksheets}
totalTime={getTotalTime()}
loading={worksheetsLoading}
/>
</div>
</div>
</div>
</td>
</tr>
</>
)}
<CreateWorksheetModal
isOpen={showCreateWorksheet}
onClose={() => setShowCreateWorksheet(false)}
workorder={ticket}
onCreate={handleCreateWorksheet}
/>
<StatusHistoryModal
isOpen={showHistoryModal}
onClose={() => setShowHistoryModal(false)}
worksheets={worksheets}
ticket={ticket}
/>
<tr className="spacer">
<td colSpan={10} style={{ height: '8px', background: '#fff' }}></td>
<td colSpan={10} style={{ height: '12px', background: 'transparent', border: 'none' }}></td>
</tr>
</>
)

View File

@@ -0,0 +1,242 @@
import { useState } from 'react'
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp } from 'react-icons/fa'
export default function WorksheetList({ worksheets, totalTime, loading }) {
const [expandedWorksheets, setExpandedWorksheets] = useState({})
if (loading) {
return (
<div className="text-center p-4">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
)
}
if (!worksheets || worksheets.length === 0) {
return (
<div className="alert alert-info" role="alert">
<FaComment className="me-2" />
Noch keine Worksheets vorhanden. Erstelle das erste Worksheet für dieses Ticket.
</div>
)
}
const formatTime = (minutes) => {
if (!minutes || minutes === 0) return '-'
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
}
const formatDateTime = (date, time) => {
if (!time) return date
const hours = time.substring(0, 2)
const mins = time.substring(2, 4)
return `${date} ${hours}:${mins}`
}
const toggleWorksheet = (wsid) => {
setExpandedWorksheets(prev => ({
...prev,
[wsid]: !prev[wsid]
}))
}
return (
<div className="worksheet-list">
{/* Worksheet-Einträge */}
<div className="timeline">
{worksheets.map((ws, index) => {
const isExpanded = expandedWorksheets[ws.wsid] || false
return (
<div key={ws.$id} className="timeline-item mb-4" style={{
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
}}>
<div className="card border-0 shadow-sm overflow-hidden" style={{
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
borderRadius: '8px',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}} onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
e.currentTarget.style.boxShadow = '0 8px 16px rgba(0,0,0,0.1)'
}} onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'
}}>
{/* Header - Immer sichtbar, klickbar */}
<div
className="card-header d-flex justify-content-between align-items-center py-3"
onClick={() => toggleWorksheet(ws.wsid)}
style={{
background: ws.isComment
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
border: 'none',
cursor: 'pointer',
userSelect: 'none',
transition: 'background 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = ws.isComment
? 'linear-gradient(135deg, #059669 0%, #047857 100%)'
: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = ws.isComment
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)'
}}
>
<div className="d-flex align-items-center gap-3">
{isExpanded ? (
<FaChevronUp style={{ fontSize: '0.9rem', opacity: 0.8 }} />
) : (
<FaChevronDown style={{ fontSize: '0.9rem', opacity: 0.8 }} />
)}
<div>
<strong className="fs-6">WSID {ws.wsid}</strong>
{ws.isComment && (
<span className="badge ms-2" style={{
background: 'rgba(255,255,255,0.3)'
}}>
<FaComment className="me-1" /> Kommentar
</span>
)}
</div>
{/* Collapsed: Mitarbeiter & Zeit im Header */}
{!isExpanded && (
<div className="d-flex align-items-center gap-3 ms-3">
<div className="d-flex align-items-center">
<FaUser style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
<span style={{ fontSize: '0.9rem' }}>{ws.employeeName}</span>
</div>
{!ws.isComment && (
<div className="d-flex align-items-center">
<FaClock style={{ fontSize: '0.9rem', marginRight: '0.5rem' }} />
<span style={{ fontSize: '0.9rem' }}>{formatTime(ws.totalTime)}</span>
</div>
)}
<span className="badge" style={{
background: 'rgba(255,255,255,0.2)',
fontSize: '0.8rem'
}}>{ws.serviceType}</span>
</div>
)}
</div>
<small style={{ opacity: 0.9 }}>
{formatDateTime(ws.startDate, ws.startTime)}
</small>
</div>
{/* Body - Nur wenn expanded */}
{isExpanded && (
<div
className="card-body p-4"
style={{
animation: 'slideDown 0.3s ease-out'
}}
>
{/* Mitarbeiter & Zeit */}
<div className="row mb-3">
<div className="col-md-6">
<div className="d-flex align-items-center">
<FaUser className="me-2" style={{ color: '#10b981' }} />
<strong>{ws.employeeName}</strong>
{ws.employeeShort && (
<span className="badge ms-2" style={{
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
color: 'white'
}}>{ws.employeeShort}</span>
)}
</div>
</div>
<div className="col-md-6 text-md-end">
{!ws.isComment && (
<div className="d-flex align-items-center justify-content-md-end">
<FaClock className="me-2" style={{ color: '#10b981' }} />
<strong className="fs-5" style={{ color: '#10b981' }}>{formatTime(ws.totalTime)}</strong>
</div>
)}
</div>
</div>
{/* Service Type */}
<div className="mb-3">
<span className="badge px-3 py-2" style={{
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
color: 'white',
fontSize: '0.85rem'
}}>{ws.serviceType}</span>
</div>
{/* Status-Änderung */}
{ws.oldStatus !== ws.newStatus && (
<div className="mb-3 p-3 rounded-3" style={{
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%)'
}}>
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
<span className="text-muted">Status:</span>{' '}
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldStatus}</span>
<span className="mx-2" style={{ color: '#10b981' }}></span>
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newStatus}</span>
</div>
)}
{/* Response Level-Änderung */}
{ws.oldResponseLevel && ws.newResponseLevel && ws.oldResponseLevel !== ws.newResponseLevel && (
<div className="mb-3">
<span className="text-muted">Response Level:</span>{' '}
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldResponseLevel}</span>
<span className="mx-2"></span>
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newResponseLevel}</span>
</div>
)}
{/* Details */}
<div className="mt-3 p-3 rounded-3" style={{
background: 'rgba(16, 185, 129, 0.05)',
border: '1px solid rgba(16, 185, 129, 0.1)'
}}>
<small className="text-dark" style={{ whiteSpace: 'pre-wrap', lineHeight: '1.6' }}>
{ws.details}
</small>
</div>
</div>
)}
</div>
</div>
)
})}
</div>
<style>{`
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
max-height: 1000px;
transform: translateY(0);
}
}
`}</style>
</div>
)
}

View File

@@ -0,0 +1,312 @@
import { FaClock, FaUsers, FaChartLine } from 'react-icons/fa'
export default function WorksheetStats({ worksheets }) {
if (!worksheets || worksheets.length === 0) {
return null
}
// Gesamtarbeitszeit
const totalMinutes = worksheets
.filter(ws => !ws.isComment)
.reduce((sum, ws) => sum + (ws.totalTime || 0), 0)
// Nach Mitarbeiter gruppieren
const byEmployee = worksheets.reduce((acc, ws) => {
const empId = ws.employeeId
if (!acc[empId]) {
acc[empId] = {
name: ws.employeeName,
short: ws.employeeShort,
time: 0,
count: 0
}
}
if (!ws.isComment) {
acc[empId].time += ws.totalTime || 0
}
acc[empId].count += 1
return acc
}, {})
// Service Type Verteilung
const byServiceType = worksheets.reduce((acc, ws) => {
const type = ws.serviceType || 'Unknown'
acc[type] = (acc[type] || 0) + 1
return acc
}, {})
const formatTime = (minutes) => {
if (!minutes || minutes === 0) return '0min'
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
}
return (
<div className="worksheet-stats" style={{
display: 'flex',
flexDirection: 'column',
gap: '16px',
height: '100%'
}}>
{/* Gesamtübersicht */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '0 0 auto'
}}>
<h6 style={{
color: 'var(--dark-text)',
marginBottom: '12px',
fontSize: '14px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<FaChartLine size={16} style={{ color: '#10b981' }} />
Gesamtübersicht
</h6>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Worksheets:</span>
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.length}</strong>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Arbeitszeit:</span>
<strong style={{ color: '#10b981', fontSize: '16px' }}>{formatTime(totalMinutes)}</strong>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Kommentare:</span>
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>{worksheets.filter(ws => ws.isComment).length}</strong>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<span style={{ color: 'rgba(226, 232, 240, 0.8)', fontSize: '12px' }}>Ø pro WS:</span>
<strong style={{ color: 'var(--dark-text)', fontSize: '16px' }}>
{formatTime(Math.round(totalMinutes / (worksheets.filter(ws => !ws.isComment).length || 1)))}
</strong>
</div>
</div>
</div>
{/* Mitarbeiter Kombiniertes Diagramm */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '1 1 auto',
minHeight: '200px'
}}>
<h6 style={{
color: 'var(--dark-text)',
marginBottom: '16px',
fontSize: '14px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<FaUsers size={16} style={{ color: '#10b981' }} />
Mitarbeiter-Statistiken
</h6>
{Object.keys(byEmployee).length === 0 ? (
<div style={{ color: '#a0aec0', textAlign: 'center', padding: '20px' }}>
Keine Mitarbeiter-Daten verfügbar
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{(() => {
const employeeArray = Object.values(byEmployee).sort((a, b) => b.time - a.time)
const maxTime = Math.max(...employeeArray.map(e => e.time), 1)
return employeeArray.map((emp, idx) => {
const percentage = maxTime > 0 ? (emp.time / maxTime) * 100 : 0
return (
<div key={idx}>
<div style={{
display: 'flex',
alignItems: 'center',
marginBottom: '6px',
fontSize: '11px'
}}>
<span style={{
color: 'var(--dark-text)',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '6px',
minWidth: '120px'
}}>
{emp.short && (
<span style={{
background: 'rgba(16, 185, 129, 0.2)',
color: '#10b981',
padding: '2px 6px',
borderRadius: '3px',
fontSize: '9px',
fontWeight: 'bold'
}}>{emp.short}</span>
)}
<span style={{ maxWidth: '100px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{emp.name}
</span>
</span>
</div>
<div style={{
width: '100%',
height: '28px',
background: 'rgba(26, 32, 44, 0.6)',
borderRadius: '6px',
overflow: 'hidden',
position: 'relative',
display: 'flex',
alignItems: 'center'
}}>
{/* WS Anzahl am Anfang */}
<div style={{
position: 'absolute',
left: '8px',
zIndex: 2,
color: 'white',
fontSize: '11px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<span>WS</span>
<span style={{
background: 'rgba(255, 255, 255, 0.3)',
padding: '2px 6px',
borderRadius: '4px'
}}>{emp.count}</span>
</div>
{/* Balken mit Zeit */}
<div style={{
width: `${percentage}%`,
height: '100%',
background: 'linear-gradient(90deg, #10b981 0%, #059669 100%)',
borderRadius: '6px',
transition: 'width 0.5s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
paddingRight: '8px',
paddingLeft: '60px',
boxShadow: '0 2px 8px rgba(16, 185, 129, 0.3)',
position: 'relative'
}}>
{/* Zeit am Ende des Balkens */}
<span style={{
color: 'white',
fontSize: '11px',
fontWeight: 'bold',
whiteSpace: 'nowrap'
}}>
{formatTime(emp.time)}
</span>
</div>
{/* Zeit außerhalb des Balkens (falls Balken zu kurz) */}
{percentage < 30 && (
<div style={{
position: 'absolute',
right: '8px',
zIndex: 2,
color: '#10b981',
fontSize: '11px',
fontWeight: 'bold'
}}>
{formatTime(emp.time)}
</div>
)}
</div>
</div>
)
})
})()}
</div>
)}
</div>
{/* Service Type Verteilung */}
<div style={{
background: 'rgba(45, 55, 72, 0.5)',
borderRadius: '12px',
padding: '16px',
border: '1px solid rgba(16, 185, 129, 0.2)',
flex: '0 0 auto'
}}>
<h6 style={{
color: 'var(--dark-text)',
marginBottom: '12px',
fontSize: '14px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<FaClock size={16} style={{ color: '#10b981' }} />
Service Types
</h6>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{Object.entries(byServiceType).map(([type, count]) => (
<div key={type} style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px',
background: 'rgba(26, 32, 44, 0.4)',
borderRadius: '6px'
}}>
<span style={{
color: 'var(--dark-text)',
fontSize: '12px',
fontWeight: '500'
}}>{type}</span>
<strong style={{
color: '#10b981',
fontSize: '14px'
}}>{count}</strong>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -1,11 +1,46 @@
import { createContext, useContext, useState, useEffect } from 'react'
import { account } from '../lib/appwrite'
import { account, databases, DATABASE_ID, COLLECTIONS, ID, Query } from '../lib/appwrite'
const AuthContext = createContext()
// Demo mode when Appwrite is not configured
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu
async function ensureEmployeeExists(user) {
if (!user || DEMO_MODE) return
try {
// Prüfe ob User bereits in employees Collection existiert
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
[Query.equal('userId', user.$id)]
)
// Wenn User noch nicht existiert, füge ihn hinzu
if (response.documents.length === 0) {
await databases.createDocument(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
ID.unique(),
{
userId: user.$id,
displayName: user.name || user.email,
email: user.email,
shortcode: '' // Kürzel wird später vom Admin hinzugefügt
}
)
console.log('✅ User automatisch zur Mitarbeiter-Liste hinzugefügt')
}
} catch (error) {
// Fehler ignorieren wenn Collection nicht existiert oder Permissions fehlen
if (error.code !== 404) {
console.warn('Could not add user to employees collection:', error.message)
}
}
}
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
@@ -28,7 +63,14 @@ export function AuthProvider({ children }) {
try {
const session = await account.get()
setUser(session)
// Automatisch zur employees Collection hinzufügen
await ensureEmployeeExists(session)
} catch (error) {
// Kein Fehler loggen beim initialen Check - das ist normal wenn nicht eingeloggt
// Nur loggen wenn es ein unerwarteter Fehler ist (nicht 401)
if (error.code !== 401 && error.code !== 404) {
console.error('Unexpected error checking user:', error)
}
setUser(null)
} finally {
setLoading(false)
@@ -45,11 +87,38 @@ export function AuthProvider({ children }) {
}
try {
await account.createEmailPasswordSession(email, password)
await checkUser()
// Appwrite 1.5.7 / SDK 13.0 - versuche beide Methoden für Kompatibilität
try {
await account.createEmailSession(email, password)
} catch (e) {
// Fallback für ältere API
if (account.createEmailPasswordSession) {
await account.createEmailPasswordSession(email, password)
} else {
throw e
}
}
// User-Daten laden und automatisch zur employees Collection hinzufügen
const session = await account.get()
setUser(session)
await ensureEmployeeExists(session)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
console.error('Login error:', error)
let errorMessage = error.message || 'Login fehlgeschlagen'
// Bessere Fehlermeldungen
if (error.code === 401 || errorMessage.includes('Invalid credentials')) {
errorMessage = 'Ungültige Email oder Passwort'
} else if (errorMessage.includes('User not found')) {
errorMessage = 'Benutzer nicht gefunden. Bitte registriere dich zuerst.'
} else if (errorMessage.includes('Email/Password')) {
errorMessage = 'Email/Password Authentifizierung ist nicht aktiviert. Bitte aktiviere sie in deinem Appwrite Dashboard unter Auth → Providers.'
}
return { success: false, error: errorMessage }
}
}
@@ -68,18 +137,11 @@ export function AuthProvider({ children }) {
}
}
async function register(email, password, name) {
if (DEMO_MODE) {
return login(email, password)
}
try {
await account.create('unique()', email, password, name)
await login(email, password)
return { success: true }
} catch (error) {
return { success: false, error: error.message }
}
// Hilfsfunktion um zu prüfen ob Benutzer Admin ist
const isAdmin = () => {
if (!user) return false
// Prüfe ob Benutzer das "admin" Label hat
return user.labels?.includes('admin') || false
}
const value = {
@@ -87,7 +149,7 @@ export function AuthProvider({ children }) {
loading,
login,
logout,
register
isAdmin: isAdmin()
}
return (

152
src/hooks/useAdminConfig.js Normal file
View File

@@ -0,0 +1,152 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, ID } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Default-Werte für Demo-Modus
const DEFAULT_CONFIG = {
ticketTypes: [
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
'Rollout', 'Emergency Call', 'Other Services'
],
systems: [
'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware',
'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network',
'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking',
'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a'
],
responseLevels: [
'USER', 'KEY USER', 'Helpdesk', 'Support', 'Admin', 'FS/FE', '24/7',
'TECH MGMT', 'Backoffice', 'BUSI MGMT', 'n/a'
],
serviceTypes: ['Remote', 'On Site', 'Off Site'],
priorities: [
{ value: 0, label: 'None' },
{ value: 1, label: 'Low' },
{ value: 2, label: 'Medium' },
{ value: 3, label: 'High' },
{ value: 4, label: 'Critical' }
]
}
export function useAdminConfig() {
const [config, setConfig] = useState(DEFAULT_CONFIG)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchConfig = useCallback(async () => {
if (DEMO_MODE) {
setConfig(DEFAULT_CONFIG)
setLoading(false)
return
}
try {
// Versuche Config-Dokument zu laden (ID: 'config')
try {
const doc = await databases.getDocument(
DATABASE_ID,
COLLECTIONS.CONFIG || 'config',
'config'
)
setConfig({
ticketTypes: doc.ticketTypes || DEFAULT_CONFIG.ticketTypes,
systems: doc.systems || DEFAULT_CONFIG.systems,
responseLevels: doc.responseLevels || DEFAULT_CONFIG.responseLevels,
serviceTypes: doc.serviceTypes || DEFAULT_CONFIG.serviceTypes,
priorities: doc.priorities || DEFAULT_CONFIG.priorities
})
} catch (e) {
// Config existiert noch nicht (404) - das ist normal, verwende Defaults
if (e.code === 404 || e.message?.includes('not found')) {
setConfig(DEFAULT_CONFIG)
setError(null) // Kein Fehler, Collection existiert einfach noch nicht
} else {
throw e
}
}
setError(null)
} catch (err) {
console.error('Error fetching config:', err)
// Nur echte Fehler als Error setzen, nicht 404
if (err.code !== 404 && !err.message?.includes('not found')) {
setError(err.message)
} else {
setError(null)
}
setConfig(DEFAULT_CONFIG) // Fallback zu Defaults
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchConfig()
}, [fetchConfig])
const updateConfig = async (newConfig) => {
if (DEMO_MODE) {
setConfig(newConfig)
localStorage.setItem('admin_config', JSON.stringify(newConfig))
return { success: true }
}
try {
const configData = {
ticketTypes: newConfig.ticketTypes,
systems: newConfig.systems,
responseLevels: newConfig.responseLevels,
serviceTypes: newConfig.serviceTypes,
priorities: newConfig.priorities
}
try {
// Versuche zu aktualisieren
await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.CONFIG || 'config',
'config',
configData
)
} catch (e) {
// Dokument existiert nicht (404) oder Collection existiert nicht
if (e.code === 404 || e.message?.includes('not found')) {
// Versuche zu erstellen
try {
await databases.createDocument(
DATABASE_ID,
COLLECTIONS.CONFIG || 'config',
'config',
configData
)
} catch (createErr) {
// Collection existiert nicht - zeige hilfreiche Fehlermeldung
if (createErr.code === 404 || createErr.message?.includes('Collection')) {
throw new Error('Die "config" Collection existiert noch nicht. Bitte erstelle sie zuerst in Appwrite.')
}
throw createErr
}
} else {
throw e
}
}
setConfig(newConfig)
return { success: true }
} catch (err) {
console.error('Error updating config:', err)
return { success: false, error: err.message }
}
}
return {
config,
loading,
error,
updateConfig,
refresh: fetchConfig
}
}

121
src/hooks/useCustomers.js Normal file
View File

@@ -0,0 +1,121 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, DATABASE_ID, COLLECTIONS, ID, Query } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo-Kunden für Testing
const DEMO_CUSTOMERS = [
{ $id: '1', code: 'C001', name: 'Kunde A', location: 'Berlin', email: 'kunde.a@example.com', phone: '030-123456' },
{ $id: '2', code: 'C002', name: 'Kunde B', location: 'München', email: 'kunde.b@example.com', phone: '089-654321' }
]
export function useCustomers() {
const [customers, setCustomers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const fetchCustomers = useCallback(async () => {
if (DEMO_MODE) {
setCustomers(DEMO_CUSTOMERS)
setLoading(false)
return
}
try {
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
[Query.orderAsc('name')]
)
setCustomers(response.documents)
setError(null)
} catch (err) {
console.error('Error fetching customers:', err)
// Wenn Collection nicht existiert, setze leeres Array (kein Fehler)
if (err.code === 404 || err.message?.includes('not found')) {
setCustomers([])
setError(null) // Kein Fehler, Collection existiert einfach noch nicht
} else {
setError(err.message)
setCustomers([])
}
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchCustomers()
}, [fetchCustomers])
const createCustomer = async (data) => {
if (DEMO_MODE) {
const newCustomer = { ...data, $id: Date.now().toString() }
setCustomers(prev => [...prev, newCustomer])
return { success: true, data: newCustomer }
}
try {
const response = await databases.createDocument(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
ID.unique(),
data
)
setCustomers(prev => [...prev, response])
return { success: true, data: response }
} catch (err) {
return { success: false, error: err.message }
}
}
const updateCustomer = async (id, data) => {
if (DEMO_MODE) {
setCustomers(prev => prev.map(c => c.$id === id ? { ...c, ...data } : c))
return { success: true }
}
try {
const response = await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
id,
data
)
setCustomers(prev => prev.map(c => c.$id === id ? response : c))
return { success: true, data: response }
} catch (err) {
return { success: false, error: err.message }
}
}
const deleteCustomer = async (id) => {
if (DEMO_MODE) {
setCustomers(prev => prev.filter(c => c.$id !== id))
return { success: true }
}
try {
await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.CUSTOMERS,
id
)
setCustomers(prev => prev.filter(c => c.$id !== id))
return { success: true }
} catch (err) {
return { success: false, error: err.message }
}
}
return {
customers,
loading,
error,
refresh: fetchCustomers,
createCustomer,
updateCustomer,
deleteCustomer
}
}

207
src/hooks/useEmployees.js Normal file
View File

@@ -0,0 +1,207 @@
import { useState, useEffect, useCallback } from 'react'
import { databases, account, DATABASE_ID, COLLECTIONS, ID, Query } from '../lib/appwrite'
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
// Demo-Mitarbeiter für Testing
const DEMO_EMPLOYEES = [
{ $id: '1', userId: 'user1', displayName: 'Kenso Grimm', email: 'kenso@example.com', shortcode: 'KNSO' },
{ $id: '2', userId: 'user2', displayName: 'Christian Lehmann', email: 'christian@example.com', shortcode: 'CHLE' }
]
export function useEmployees() {
const [employees, setEmployees] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [syncing, setSyncing] = useState(false)
const fetchEmployees = useCallback(async () => {
setLoading(true)
if (DEMO_MODE) {
setEmployees(DEMO_EMPLOYEES)
setLoading(false)
return
}
try {
const response = await databases.listDocuments(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
[Query.orderAsc('displayName')]
)
setEmployees(response.documents)
setError(null)
} catch (err) {
console.error('Error fetching employees:', err)
// Wenn Collection nicht existiert, setze leeres Array (kein Fehler)
if (err.code === 404 || err.message?.includes('not found')) {
setEmployees([])
setError(null) // Kein Fehler, Collection existiert einfach noch nicht
} else {
setError(err.message)
setEmployees([])
}
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchEmployees()
}, [fetchEmployees])
const createEmployee = async (data) => {
if (DEMO_MODE) {
const newEmployee = { ...data, $id: Date.now().toString() }
setEmployees(prev => [...prev, newEmployee])
return { success: true, data: newEmployee }
}
try {
// Validierung
if (!data.userId || !data.displayName) {
return { success: false, error: 'userId und displayName sind erforderlich' }
}
const response = await databases.createDocument(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
ID.unique(),
{
userId: data.userId,
displayName: data.displayName,
email: data.email || '',
shortcode: data.shortcode || ''
}
)
setEmployees(prev => [...prev, response])
return { success: true, data: response }
} catch (err) {
console.error('Error creating employee:', err)
return { success: false, error: err.message }
}
}
const updateEmployee = async (id, data) => {
if (DEMO_MODE) {
setEmployees(prev => prev.map(e => e.$id === id ? { ...e, ...data } : e))
return { success: true }
}
try {
const response = await databases.updateDocument(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
id,
data
)
setEmployees(prev => prev.map(e => e.$id === id ? response : e))
return { success: true, data: response }
} catch (err) {
console.error('Error updating employee:', err)
return { success: false, error: err.message }
}
}
const deleteEmployee = async (id) => {
if (DEMO_MODE) {
setEmployees(prev => prev.filter(e => e.$id !== id))
return { success: true }
}
try {
await databases.deleteDocument(
DATABASE_ID,
COLLECTIONS.EMPLOYEES,
id
)
setEmployees(prev => prev.filter(e => e.$id !== id))
return { success: true }
} catch (err) {
console.error('Error deleting employee:', err)
return { success: false, error: err.message }
}
}
/**
* Synchronisiert Appwrite Auth Users mit der employees Collection
* Erstellt fehlende Einträge für neue Users
*/
const syncWithAuthUsers = async () => {
if (DEMO_MODE) {
return { success: true, message: 'Demo-Modus: Keine Synchronisierung nötig' }
}
setSyncing(true)
try {
// 1. Lade alle Appwrite Auth Users
// Hinweis: In Appwrite 1.5.7 gibt es möglicherweise keine direkte List-Users API
// für normale User. Diese Funktion benötigt Server-Side Code oder Admin-API-Key.
// Für jetzt implementieren wir einen Workaround: Wir bieten ein manuelles Add-Interface.
// Alternative: Wenn der User Appwrite Admin ist, können wir versuchen:
// const users = await account.listUsers() // Funktioniert nur mit Admin-Rechten
// Da das nicht direkt möglich ist, geben wir eine Info zurück
return {
success: false,
error: 'Automatische Synchronisierung erfordert Admin-API-Zugriff. Bitte füge Mitarbeiter manuell hinzu oder verwende die Appwrite Server API.'
}
} catch (err) {
console.error('Error syncing auth users:', err)
return { success: false, error: err.message }
} finally {
setSyncing(false)
}
}
/**
* Erstellt einen Employee-Eintrag für den aktuell eingeloggten User
* Nützlich für Self-Service
*/
const createSelfEmployee = async (shortcode = '') => {
if (DEMO_MODE) {
return { success: true, message: 'Demo-Modus' }
}
try {
// Hole aktuellen User
const currentUser = await account.get()
// Prüfe, ob Employee bereits existiert
const existing = employees.find(e => e.userId === currentUser.$id)
if (existing) {
return { success: false, error: 'Mitarbeiter-Eintrag existiert bereits' }
}
// Erstelle Employee-Eintrag
const result = await createEmployee({
userId: currentUser.$id,
displayName: currentUser.name || currentUser.email,
email: currentUser.email,
shortcode: shortcode
})
return result
} catch (err) {
console.error('Error creating self employee:', err)
return { success: false, error: err.message }
}
}
return {
employees,
loading,
error,
syncing,
refresh: fetchEmployees,
createEmployee,
updateEmployee,
deleteEmployee,
syncWithAuthUsers,
createSelfEmployee
}
}

Some files were not shown because too many files have changed in this diff Show More