Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a8604f7e6 | ||
|
|
34c4db0228 | ||
|
|
062d992b63 | ||
|
|
bf007df737 | ||
|
|
7b33e3900d | ||
|
|
7852593a7a | ||
|
|
df62bcbc55 | ||
|
|
b758b1f9a7 | ||
|
|
889a2f42eb | ||
|
|
d684aea576 | ||
|
|
8d2a99e5d6 | ||
|
|
cbf77004ad | ||
|
|
2d4e569cc3 | ||
|
|
0c884a57d7 | ||
|
|
149b38b94b | ||
|
|
e678899141 | ||
|
|
8cb0bb8146 | ||
|
|
d6becf0151 | ||
|
|
d86f9c07cd | ||
|
|
cf1d653eb2 | ||
|
|
d42ee939e9 | ||
|
|
19464fe20d | ||
|
|
98601b4b3d | ||
|
|
3cc6ab3343 | ||
|
|
d7f9cf75a3 | ||
|
|
5361800ca9 | ||
|
|
52713fb481 | ||
|
|
e59a066d3c | ||
|
|
fe3e39bd04 | ||
|
|
4c305fcd46 | ||
|
|
0b24596f15 | ||
|
|
1cec319fa8 | ||
|
|
5786eed3ba | ||
|
|
5270084ff0 | ||
|
|
8855fe92ba | ||
|
|
612063455e | ||
|
|
9e4c60fdaf | ||
|
|
86fb24b1a9 | ||
|
|
6c3f03b5b4 | ||
|
|
42f729abfc | ||
|
|
ff536b048f | ||
|
|
4be20237a1 | ||
|
|
f76b738f5f | ||
|
|
27b365622e | ||
|
|
a1cb90372f | ||
|
|
7d764ce6bc | ||
|
|
1006824375 | ||
|
|
43a8064ad5 | ||
|
|
d20a4de117 | ||
|
|
8f6927fbb0 | ||
|
|
8abf11ad18 | ||
|
|
4a2e94bc83 | ||
|
|
fcd13e6a40 | ||
|
|
fda673702e | ||
|
|
8d62e353cb | ||
|
|
f5257cd251 | ||
|
|
73d6c50b8d | ||
|
|
f174421c63 | ||
|
|
d2e7088146 | ||
|
|
4f4de7f290 | ||
|
|
5fbb2fb4b5 | ||
|
|
cb110a184b | ||
|
|
ee7c866616 |
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
# Öffentliche Vite-Variablen für lokale Entwicklung (in Git, kein Secret)
|
||||||
|
VITE_APPWRITE_ENDPOINT=http://localhost:5173/v1
|
||||||
|
VITE_APPWRITE_PROJECT_ID=6a1058610003c5a13a05
|
||||||
|
VITE_APPWRITE_DATABASE_ID=woms-database
|
||||||
|
VITE_APPWRITE_BUCKET_ID=woms-attachments
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Kopieren nach .env.local (wird nicht von Git versioniert):
|
||||||
|
# cp .env.example .env.local
|
||||||
|
#
|
||||||
|
# Lokal mit Vite-Proxy (empfohlen � Session-Cookies wie auf dem Server):
|
||||||
|
VITE_APPWRITE_ENDPOINT=http://localhost:5173/v1
|
||||||
|
VITE_APPWRITE_PROJECT_ID=6a1058610003c5a13a05
|
||||||
|
VITE_APPWRITE_DATABASE_ID=woms-database
|
||||||
|
VITE_APPWRITE_BUCKET_ID=woms-attachments
|
||||||
|
|
||||||
|
# Optional: Proxy-Ziel (Standard: https://appwrite.webklar.com)
|
||||||
|
# VITE_APPWRITE_PROXY_TARGET=https://appwrite.webklar.com
|
||||||
|
|
||||||
|
# Produktion / Build (ohne Vite-Proxy):
|
||||||
|
# VITE_APPWRITE_ENDPOINT=https://ticket.webklar.com/v1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Öffentliche Vite-Variablen für Production-Build (in Git, kein Secret)
|
||||||
|
VITE_APPWRITE_ENDPOINT=https://ticket.webklar.com/v1
|
||||||
|
VITE_APPWRITE_PROJECT_ID=6a1058610003c5a13a05
|
||||||
|
VITE_APPWRITE_DATABASE_ID=woms-database
|
||||||
|
VITE_APPWRITE_BUCKET_ID=woms-attachments
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables (persönliche Overrides — nicht committen)
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.production
|
.env.*.local
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"type": "static",
|
||||||
|
"branch": "test",
|
||||||
|
"displayName": "tickte-system",
|
||||||
|
"templateName": "webklar-preview-template",
|
||||||
|
"projectType": "project",
|
||||||
|
"public": false
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Lokale Entwicklung (Appwrite)
|
||||||
|
|
||||||
|
## Warum funktioniert es auf dem Server, aber nicht mit `npm run dev`?
|
||||||
|
|
||||||
|
| | Server (`ticket.webklar.com`) | Lokal (`localhost:5173`) |
|
||||||
|
|---|---|---|
|
||||||
|
| App-URL | `https://ticket.webklar.com` | `http://localhost:5173` |
|
||||||
|
| API-URL | `https://ticket.webklar.com/v1` (nginx-Proxy) | oft `https://ticket.webklar.com/v1` direkt |
|
||||||
|
| **Gleiche Origin?** | Ja | **Nein** (Cross-Origin) |
|
||||||
|
| Session-Cookie | `domain=.ticket.webklar.com` | Browser speichert Cookie **nicht** f�r localhost |
|
||||||
|
|
||||||
|
Auf dem Server leitet nginx `/v1/` an Appwrite weiter und schreibt Cookies auf `ticket.webklar.com` um. Die React-App und die API sind **dieselbe Site** � Login funktioniert.
|
||||||
|
|
||||||
|
Lokal ruft der Browser die API auf einer **anderen Domain** auf. Appwrite setzt Cookies f�r `.ticket.webklar.com`. Die werden bei Requests von `localhost:5173` **nicht mitgeschickt** ? `guests missing scopes`, Login scheint tot.
|
||||||
|
|
||||||
|
CORS f�r `http://localhost:5173` ist auf dem Server erlaubt � das reicht allein nicht f�r HttpOnly-Session-Cookies.
|
||||||
|
|
||||||
|
## L�sung: Vite-Proxy + `.env.local`
|
||||||
|
|
||||||
|
1. Datei anlegen (nicht in Git):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In `.env.local` muss stehen:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_APPWRITE_ENDPOINT=http://localhost:5173/v1
|
||||||
|
VITE_APPWRITE_PROJECT_ID=6a1058610003c5a13a05
|
||||||
|
VITE_APPWRITE_DATABASE_ID=woms-database
|
||||||
|
VITE_APPWRITE_BUCKET_ID=woms-attachments
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Dev-Server starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`vite.config.js` leitet `/v1` an `https://ticket.webklar.com` weiter und schreibt Cookie-Domains auf `localhost` um � Verhalten wie nginx auf dem Server.
|
||||||
|
|
||||||
|
## Fehler: „Failed to load resource“ / **504** in der Konsole
|
||||||
|
|
||||||
|
Typisch bei `VITE_APPWRITE_ENDPOINT=http://localhost:5173/v1`: Der Vite-Proxy erreicht Appwrite nicht rechtzeitig.
|
||||||
|
|
||||||
|
1. **Neuesten Code holen** (`git pull`) — Proxy zeigt jetzt direkt auf `appwrite.webklar.com` mit längerem Timeout.
|
||||||
|
2. Dev-Server **komplett stoppen** (Strg+C) und neu starten: `npm run dev`
|
||||||
|
3. In `.env.local` prüfen:
|
||||||
|
```env
|
||||||
|
VITE_APPWRITE_ENDPOINT=http://localhost:5173/v1
|
||||||
|
```
|
||||||
|
4. Im Browser **Network**-Tab: Welche URL liefert 504?
|
||||||
|
- `http://localhost:5173/v1/...` ? Proxy/Netzwerk (Firewall, VPN, Server nicht erreichbar)
|
||||||
|
- `https://ticket.webklar.com/v1/...` ? ohne Proxy; Endpoint in `.env.local` auf `localhost:5173` stellen
|
||||||
|
5. Test im Terminal (ersetzt 5173 durch deinen Vite-Port):
|
||||||
|
```bash
|
||||||
|
curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:5173/v1/health \
|
||||||
|
-H "X-Appwrite-Project: 6a1058610003c5a13a05"
|
||||||
|
```
|
||||||
|
Erwartung: **401** (nicht 504) — dann ist der Proxy ok.
|
||||||
|
|
||||||
|
**Notfall ohne Proxy:** In `.env.local` temporär `VITE_APPWRITE_ENDPOINT=https://ticket.webklar.com/v1` — Login-Cookies funktionieren lokal dann oft nicht, aber du siehst ob der Server erreichbar ist.
|
||||||
|
|
||||||
|
## Checkliste bei Problemen
|
||||||
|
|
||||||
|
- [ ] `.env.local` existiert (`.env` allein reicht; Vite l�dt `.env.local` mit h�herer Priorit�t)
|
||||||
|
- [ ] Endpoint ist `http://localhost:5173/v1`, **nicht** `https://appwrite.webklar.com/v1` (altes Projekt)
|
||||||
|
- [ ] Nach Login: DevTools ? Application ? Cookies ? `localhost` ? `a_session_6a1058610003c5a13a05`
|
||||||
|
- [ ] Appwrite Console ? Projekt **Ticket-System** ? Platforms ? `localhost` eingetragen
|
||||||
|
|
||||||
|
## Git / Deploy
|
||||||
|
|
||||||
|
- `.env` und `.env.local` sind in `.gitignore` � jeder Entwickler braucht eigene `.env.local`
|
||||||
|
- Server-Build nutzt Container-Env (`VITE_APPWRITE_ENDPOINT=https://ticket.webklar.com/v1`)
|
||||||
|
- Push nach Gitea deployt nur den **Frontend-Code**, nicht deine lokale `.env.local`
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,49 +1,42 @@
|
|||||||
# WOMS 2.0 - Work Order Management System
|
# WOMS – Ticketsystem von WEBklar
|
||||||
|
|
||||||
React-basiertes Ticket-Management-System mit Appwrite Backend.
|
Internes Ticketsystem unter https://ticket.webklar.com: Tickets, Arbeitsblätter mit
|
||||||
|
Zeiterfassung, Kunden und Leads, Website-Projekte, Planboard und Rechnungen (nur
|
||||||
|
anzeigen; Rechnungen werden in InvoiceNinja geschrieben).
|
||||||
|
React/Vite im Browser, Daten in Appwrite (Projekt `6a1058610003c5a13a05`,
|
||||||
|
Datenbank `woms-database`, API unter `https://ticket.webklar.com/v1`).
|
||||||
|
|
||||||
## 🚀 Schnellstart
|
## Entwickeln und live stellen
|
||||||
|
|
||||||
### 1. Dependencies installieren
|
- Lokal starten: siehe [LOCAL_DEV.md](./LOCAL_DEV.md) (Vite-Proxy + `.env.local`).
|
||||||
```bash
|
- Live ist der Branch `test` im Gitea-Repo `JUSN/tickte-system`. Ein Push löst
|
||||||
npm install
|
auf dem Server Build und Deploy aus.
|
||||||
```
|
- Achtung: Der Webhook baut **jeden** gepushten Branch und stellt ihn live.
|
||||||
|
Feature-Branches deshalb nicht pushen; live geht es mit
|
||||||
|
`git push origin <branch>:test`.
|
||||||
|
|
||||||
### 2. Appwrite einrichten
|
## Datenmodell (Kurzfassung)
|
||||||
|
|
||||||
**Wichtig:** Für eine detaillierte Schritt-für-Schritt-Anleitung siehe [APPWRITE_SETUP.md](./APPWRITE_SETUP.md)
|
Datum speichert WOMS als Text `dd.mm.yyyy`, Uhrzeiten als `hhmm` (z. B. `0930`).
|
||||||
|
|
||||||
Kurzfassung:
|
| Collection | Inhalt | Wichtige Felder |
|
||||||
1. Erstelle ein Appwrite-Projekt auf [cloud.appwrite.io](https://cloud.appwrite.io)
|
|---|---|---|
|
||||||
2. Erstelle eine `.env` Datei im Root-Verzeichnis:
|
| `workorders` | Tickets | `woid` (fortlaufend ab 10000), `topic`, `details`, `status`, `priority`, `type` (Webpage, Migration, Project, Akquise), `serviceType`, `customerId`/`customerName`, `assignedTo`/`assignedName`, `requestedBy`, `startDate`/`startTime`/`endTime`, `deadline` |
|
||||||
```env
|
| `worksheets` | Arbeitsblätter | `wsid` (fortlaufend ab 100000), `woid` + `workorderId`, `employeeId`/`employeeName`, `serviceType`, `oldStatus`/`newStatus`, `totalTime` (Minuten), `startDate`/`startTime`/`endDate`/`endTime`, `details`, `isComment`, `countsToPlan` (Abo-Stunden) |
|
||||||
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
| `customers` | Kunden und Leads | `code` (Kundennummer), `name`/`companyName`, `customerStatus` (z. B. lead, customer, lost), `archived`, Rechnungsadresse, `invoiceNinjaClientId`, Portal-Zugang |
|
||||||
VITE_APPWRITE_PROJECT_ID=deine-project-id-hier
|
| `employees` | Mitarbeiter | `userId` (Appwrite-Benutzer), `displayName`, `email`, `shortcode` (Kürzel) |
|
||||||
VITE_APPWRITE_DATABASE_ID=woms-database
|
| `websiteProjects` | Website-Projekte und Vorschauen | `customerId` (steuert, was der Kunde im Portal sieht), `ticketId` (Ziel der Git-Push-Arbeitsblätter), Subdomain, Repo |
|
||||||
```
|
| `leads`, `leadPipeline`, `leadSettings` | Lead-Automatik | Recherche-Ergebnisse, Pipeline-Schritte, Sucheinstellungen |
|
||||||
3. Folge der detaillierten Anleitung in `APPWRITE_SETUP.md` um die Datenbank-Struktur zu erstellen
|
| `roadmaps`, `roadmapNodes` | Projektpläne | Plan mit Schritten, verknüpft mit einem Ticket |
|
||||||
|
| `config` | alte Einstellungen | wird von WOMS nicht mehr gelesen (Prioritäten, alte Listen) |
|
||||||
|
|
||||||
### 3. App starten
|
Zusammenspiel mit anderen Systemen:
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Die App läuft dann auf `http://localhost:5173`
|
- **Kundenportal** (project.webklar.com): legt bei jedem Git-Push ein Arbeitsblatt an
|
||||||
|
(`employeeId` `gitea`, `serviceType` `GIT`, ohne Beginn). Die Zeit wird in WOMS
|
||||||
## 📚 Dokumentation
|
nachgetragen, danach zählt sie als Arbeitszeit. Termine aus der Rechnungsanalyse
|
||||||
|
landen am Akquise-Ticket des Kunden und erscheinen im Planboard. Kunden-, Mitarbeiter-
|
||||||
- **[APPWRITE_SETUP.md](./APPWRITE_SETUP.md)** - Detaillierte Anleitung zur Appwrite-Einrichtung
|
und Projektverwaltung laufen über die Admin-API des Portals.
|
||||||
- Schritt-für-Schritt Setup
|
- **ticket-integrations** (`/api/integrations`): Proxy zu InvoiceNinja (Rechnungen
|
||||||
- Datenbank-Schema
|
anzeigen, Kunden verknüpfen). Die Paperless-Routen dort nutzt WOMS nicht mehr;
|
||||||
- Berechtigungen
|
Paperless selbst ist gestoppt.
|
||||||
- Fehlerbehebung
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Ticket-Erstellung und -Verwaltung
|
|
||||||
- Status-Tracking (Open, Occupied, Assigned, Awaiting, Closed)
|
|
||||||
- Prioritäts-Management (Low, Normal, High, Critical)
|
|
||||||
- Filter und Suche
|
|
||||||
- Datei-Uploads
|
|
||||||
- Dashboard mit Statistiken
|
|
||||||
- Planboard für Terminplanung
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# 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! 🚀
|
|
||||||
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Webklar WOMS 2.0</title>
|
<title>Webklar WOMS 2.0</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=Lato|Open+Sans" rel="stylesheet">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+31
-31
@@ -1,109 +1,109 @@
|
|||||||
{
|
{
|
||||||
"hash": "ba7c7150",
|
"hash": "91e90cf6",
|
||||||
"configHash": "6a220e5a",
|
"configHash": "eaf678ca",
|
||||||
"lockfileHash": "a10f8d29",
|
"lockfileHash": "4b2b8d7d",
|
||||||
"browserHash": "db384cdc",
|
"browserHash": "114e306f",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"react": {
|
"react": {
|
||||||
"src": "../../react/index.js",
|
"src": "../../react/index.js",
|
||||||
"file": "react.js",
|
"file": "react.js",
|
||||||
"fileHash": "d41cd819",
|
"fileHash": "c1394b31",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"src": "../../react-dom/index.js",
|
"src": "../../react-dom/index.js",
|
||||||
"file": "react-dom.js",
|
"file": "react-dom.js",
|
||||||
"fileHash": "02c7f745",
|
"fileHash": "e0cfaa25",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react/jsx-dev-runtime": {
|
"react/jsx-dev-runtime": {
|
||||||
"src": "../../react/jsx-dev-runtime.js",
|
"src": "../../react/jsx-dev-runtime.js",
|
||||||
"file": "react_jsx-dev-runtime.js",
|
"file": "react_jsx-dev-runtime.js",
|
||||||
"fileHash": "beec6a15",
|
"fileHash": "4e6552af",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react/jsx-runtime": {
|
"react/jsx-runtime": {
|
||||||
"src": "../../react/jsx-runtime.js",
|
"src": "../../react/jsx-runtime.js",
|
||||||
"file": "react_jsx-runtime.js",
|
"file": "react_jsx-runtime.js",
|
||||||
"fileHash": "f6c3e40e",
|
"fileHash": "326b9f6e",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"@tabler/icons-react": {
|
"@tabler/icons-react": {
|
||||||
"src": "../../@tabler/icons-react/dist/esm/tabler-icons-react.mjs",
|
"src": "../../@tabler/icons-react/dist/esm/tabler-icons-react.mjs",
|
||||||
"file": "@tabler_icons-react.js",
|
"file": "@tabler_icons-react.js",
|
||||||
"fileHash": "11d925b5",
|
"fileHash": "ef17992e",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"appwrite": {
|
"appwrite": {
|
||||||
"src": "../../appwrite/dist/esm/sdk.js",
|
"src": "../../appwrite/dist/esm/sdk.js",
|
||||||
"file": "appwrite.js",
|
"file": "appwrite.js",
|
||||||
"fileHash": "e5052173",
|
"fileHash": "e3207492",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"clsx": {
|
"clsx": {
|
||||||
"src": "../../clsx/dist/clsx.mjs",
|
"src": "../../clsx/dist/clsx.mjs",
|
||||||
"file": "clsx.js",
|
"file": "clsx.js",
|
||||||
"fileHash": "7fc4f217",
|
"fileHash": "438277fe",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"date-fns": {
|
"date-fns": {
|
||||||
"src": "../../date-fns/esm/index.js",
|
"src": "../../date-fns/esm/index.js",
|
||||||
"file": "date-fns.js",
|
"file": "date-fns.js",
|
||||||
"fileHash": "371588f0",
|
"fileHash": "c8cdb3c6",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"date-fns/locale": {
|
"date-fns/locale": {
|
||||||
"src": "../../date-fns/esm/locale/index.js",
|
"src": "../../date-fns/esm/locale/index.js",
|
||||||
"file": "date-fns_locale.js",
|
"file": "date-fns_locale.js",
|
||||||
"fileHash": "927376cc",
|
"fileHash": "affdbcf7",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"motion/react": {
|
"motion/react": {
|
||||||
"src": "../../motion/dist/es/react.mjs",
|
"src": "../../motion/dist/es/react.mjs",
|
||||||
"file": "motion_react.js",
|
"file": "motion_react.js",
|
||||||
"fileHash": "03c47f11",
|
"fileHash": "d06adb28",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"postprocessing": {
|
"postprocessing": {
|
||||||
"src": "../../postprocessing/build/index.js",
|
"src": "../../postprocessing/build/index.js",
|
||||||
"file": "postprocessing.js",
|
"file": "postprocessing.js",
|
||||||
"fileHash": "6a1af3d4",
|
"fileHash": "a05d80d0",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react-dom/client": {
|
"react-dom/client": {
|
||||||
"src": "../../react-dom/client.js",
|
"src": "../../react-dom/client.js",
|
||||||
"file": "react-dom_client.js",
|
"file": "react-dom_client.js",
|
||||||
"fileHash": "13d89711",
|
"fileHash": "35e13db7",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"react-icons/fa": {
|
"react-icons/fa": {
|
||||||
"src": "../../react-icons/fa/index.esm.js",
|
"src": "../../react-icons/fa/index.esm.js",
|
||||||
"file": "react-icons_fa.js",
|
"file": "react-icons_fa.js",
|
||||||
"fileHash": "ce6891d0",
|
"fileHash": "b39d751a",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react-icons/fa6": {
|
"react-icons/fa6": {
|
||||||
"src": "../../react-icons/fa6/index.esm.js",
|
"src": "../../react-icons/fa6/index.esm.js",
|
||||||
"file": "react-icons_fa6.js",
|
"file": "react-icons_fa6.js",
|
||||||
"fileHash": "836a5ede",
|
"fileHash": "ee91d98a",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"react-router-dom": {
|
"react-router-dom": {
|
||||||
"src": "../../react-router-dom/dist/index.js",
|
"src": "../../react-router-dom/dist/index.js",
|
||||||
"file": "react-router-dom.js",
|
"file": "react-router-dom.js",
|
||||||
"fileHash": "65b7bd91",
|
"fileHash": "96b74880",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"tailwind-merge": {
|
"tailwind-merge": {
|
||||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||||
"file": "tailwind-merge.js",
|
"file": "tailwind-merge.js",
|
||||||
"fileHash": "8ea254d4",
|
"fileHash": "422eebfe",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"three": {
|
"three": {
|
||||||
"src": "../../three/build/three.module.js",
|
"src": "../../three/build/three.module.js",
|
||||||
"file": "three.js",
|
"file": "three.js",
|
||||||
"fileHash": "c500eef6",
|
"fileHash": "7ea78180",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -111,20 +111,20 @@
|
|||||||
"chunk-IFCYBMKG": {
|
"chunk-IFCYBMKG": {
|
||||||
"file": "chunk-IFCYBMKG.js"
|
"file": "chunk-IFCYBMKG.js"
|
||||||
},
|
},
|
||||||
"chunk-7VTGDDTZ": {
|
"chunk-DQJKJRV5": {
|
||||||
"file": "chunk-7VTGDDTZ.js"
|
"file": "chunk-DQJKJRV5.js"
|
||||||
},
|
},
|
||||||
"chunk-TDH2IRYZ": {
|
"chunk-PJEEZAML": {
|
||||||
"file": "chunk-TDH2IRYZ.js"
|
"file": "chunk-PJEEZAML.js"
|
||||||
},
|
},
|
||||||
"chunk-NMLHVZ76": {
|
"chunk-6PXSGDAH": {
|
||||||
"file": "chunk-NMLHVZ76.js"
|
"file": "chunk-6PXSGDAH.js"
|
||||||
},
|
},
|
||||||
"chunk-QRULMDK5": {
|
"chunk-DRWLMN53": {
|
||||||
"file": "chunk-QRULMDK5.js"
|
"file": "chunk-DRWLMN53.js"
|
||||||
},
|
},
|
||||||
"chunk-FSI7PPCM": {
|
"chunk-SJKHQ62W": {
|
||||||
"file": "chunk-FSI7PPCM.js"
|
"file": "chunk-SJKHQ62W.js"
|
||||||
},
|
},
|
||||||
"chunk-G3PMV62Z": {
|
"chunk-G3PMV62Z": {
|
||||||
"file": "chunk-G3PMV62Z.js"
|
"file": "chunk-G3PMV62Z.js"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../../isomorphic-form-data/lib/browser.js", "../../cross-fetch/dist/browser-ponyfill.js", "../../appwrite/node_modules/tslib/tslib.es6.js", "../../appwrite/src/service.ts", "../../appwrite/src/query.ts", "../../appwrite/src/client.ts", "../../appwrite/src/services/account.ts", "../../appwrite/src/services/avatars.ts", "../../appwrite/src/services/databases.ts", "../../appwrite/src/services/functions.ts", "../../appwrite/src/services/graphql.ts", "../../appwrite/src/services/locale.ts", "../../appwrite/src/services/storage.ts", "../../appwrite/src/services/teams.ts", "../../appwrite/src/permission.ts", "../../appwrite/src/role.ts", "../../appwrite/src/id.ts"],
|
"sources": ["../../isomorphic-form-data/lib/browser.js", "../../cross-fetch/dist/browser-ponyfill.js", "../../appwrite/node_modules/tslib/tslib.es6.js", "../../appwrite/src/service.ts", "../../appwrite/src/query.ts", "../../appwrite/src/client.ts", "../../appwrite/src/services/account.ts", "../../appwrite/src/services/avatars.ts", "../../appwrite/src/services/databases.ts", "../../appwrite/src/services/functions.ts", "../../appwrite/src/services/graphql.ts", "../../appwrite/src/services/locale.ts", "../../appwrite/src/services/storage.ts", "../../appwrite/src/services/teams.ts", "../../appwrite/src/permission.ts", "../../appwrite/src/role.ts", "../../appwrite/src/id.ts"],
|
||||||
"sourcesContent": ["module.exports = window.FormData\r\n", "var global = typeof self !== 'undefined' ? self : this;\r\nvar __self__ = (function () {\r\nfunction F() {\r\nthis.fetch = false;\r\nthis.DOMException = global.DOMException\r\n}\r\nF.prototype = global;\r\nreturn new F();\r\n})();\r\n(function(self) {\r\n\r\nvar irrelevant = (function (exports) {\r\n\r\n var support = {\r\n searchParams: 'URLSearchParams' in self,\r\n iterable: 'Symbol' in self && 'iterator' in Symbol,\r\n blob:\r\n 'FileReader' in self &&\r\n 'Blob' in self &&\r\n (function() {\r\n try {\r\n new Blob();\r\n return true\r\n } catch (e) {\r\n return false\r\n }\r\n })(),\r\n formData: 'FormData' in self,\r\n arrayBuffer: 'ArrayBuffer' in self\r\n };\r\n\r\n function isDataView(obj) {\r\n return obj && DataView.prototype.isPrototypeOf(obj)\r\n }\r\n\r\n if (support.arrayBuffer) {\r\n var viewClasses = [\r\n '[object Int8Array]',\r\n '[object Uint8Array]',\r\n '[object Uint8ClampedArray]',\r\n '[object Int16Array]',\r\n '[object Uint16Array]',\r\n '[object Int32Array]',\r\n '[object Uint32Array]',\r\n '[object Float32Array]',\r\n '[object Float64Array]'\r\n ];\r\n\r\n var isArrayBufferView =\r\n ArrayBuffer.isView ||\r\n function(obj) {\r\n return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1\r\n };\r\n }\r\n\r\n function normalizeName(name) {\r\n if (typeof name !== 'string') {\r\n name = String(name);\r\n }\r\n if (/[^a-z0-9\\-#$%&'*+.^_`|~]/i.test(name)) {\r\n throw new TypeError('Invalid character in header field name')\r\n }\r\n return name.toLowerCase()\r\n }\r\n\r\n function normalizeValue(value) {\r\n if (typeof value !== 'string') {\r\n value = String(value);\r\n }\r\n return value\r\n }\r\n\r\n // Build a destructive iterator for the value list\r\n function iteratorFor(items) {\r\n var iterator = {\r\n next: function() {\r\n var value = items.shift();\r\n return {done: value === undefined, value: value}\r\n }\r\n };\r\n\r\n if (support.iterable) {\r\n iterator[Symbol.iterator] = function() {\r\n return iterator\r\n };\r\n }\r\n\r\n return iterator\r\n }\r\n\r\n function Headers(headers) {\r\n this.map = {};\r\n\r\n if (headers instanceof Headers) {\r\n headers.forEach(function(value, name) {\r\n this.append(name, value);\r\n }, this);\r\n } else if (Array.isArray(headers)) {\r\n headers.forEach(function(header) {\r\n this.append(header[0], header[1]);\r\n }, this);\r\n } else if (headers) {\r\n Object.getOwnPropertyNames(headers).forEach(function(name) {\r\n this.append(name, headers[name]);\r\n }, this);\r\n }\r\n }\r\n\r\n Headers.prototype.append = function(name, value) {\r\n name = normalizeName(name);\r\n value = normalizeValue(value);\r\n var oldValue = this.map[name];\r\n this.map[name] = oldValue ? oldValue + ', ' + value : value;\r\n };\r\n\r\n Headers.prototype['delete'] = function(name) {\r\n delete this.map[normalizeName(name)];\r\n };\r\n\r\n Headers.prototype.get = function(name) {\r\n name = normalizeName(name);\r\n return this.has(name) ? this.map[name] : null\r\n };\r\n\r\n Headers.prototype.has = function(name) {\r\n return this.map.hasOwnProperty(normalizeName(name))\r\n };\r\n\r\n Headers.prototype.set = function(name, value) {\r\n this.map[normalizeName(name)] = normalizeValue(value);\r\n };\r\n\r\n Headers.prototype.forEach = function(callback, thisArg) {\r\n for (var name in this.map) {\r\n if (this.map.hasOwnProperty(name)) {\r\n callback.call(thisArg, this.map[name], name, this);\r\n }\r\n }\r\n };\r\n\r\n Headers.prototype.keys = function() {\r\n var items = [];\r\n this.forEach(function(value, name) {\r\n items.push(name);\r\n });\r\n return iteratorFor(items)\r\n };\r\n\r\n Headers.prototype.values = function() {\r\n var items = [];\r\n this.forEach(function(value) {\r\n items.push(value);\r\n });\r\n return iteratorFor(items)\r\n };\r\n\r\n Headers.prototype.entries = function() {\r\n var items = [];\r\n this.forEach(function(value, name) {\r\n items.push([name, value]);\r\n });\r\n return iteratorFor(items)\r\n };\r\n\r\n if (support.iterable) {\r\n Headers.prototype[Symbol.iterator] = Headers.prototype.entries;\r\n }\r\n\r\n function consumed(body) {\r\n if (body.bodyUsed) {\r\n return Promise.reject(new TypeError('Already read'))\r\n }\r\n body.bodyUsed = true;\r\n }\r\n\r\n function fileReaderReady(reader) {\r\n return new Promise(function(resolve, reject) {\r\n reader.onload = function() {\r\n resolve(reader.result);\r\n };\r\n reader.onerror = function() {\r\n rejLine truncated
|
"sourcesContent": ["module.exports = window.FormData\n", "var global = typeof self !== 'undefined' ? self : this;\nvar __self__ = (function () {\nfunction F() {\nthis.fetch = false;\nthis.DOMException = global.DOMException\n}\nF.prototype = global;\nreturn new F();\n})();\n(function(self) {\n\nvar irrelevant = (function (exports) {\n\n var support = {\n searchParams: 'URLSearchParams' in self,\n iterable: 'Symbol' in self && 'iterator' in Symbol,\n blob:\n 'FileReader' in self &&\n 'Blob' in self &&\n (function() {\n try {\n new Blob();\n return true\n } catch (e) {\n return false\n }\n })(),\n formData: 'FormData' in self,\n arrayBuffer: 'ArrayBuffer' in self\n };\n\n function isDataView(obj) {\n return obj && DataView.prototype.isPrototypeOf(obj)\n }\n\n if (support.arrayBuffer) {\n var viewClasses = [\n '[object Int8Array]',\n '[object Uint8Array]',\n '[object Uint8ClampedArray]',\n '[object Int16Array]',\n '[object Uint16Array]',\n '[object Int32Array]',\n '[object Uint32Array]',\n '[object Float32Array]',\n '[object Float64Array]'\n ];\n\n var isArrayBufferView =\n ArrayBuffer.isView ||\n function(obj) {\n return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1\n };\n }\n\n function normalizeName(name) {\n if (typeof name !== 'string') {\n name = String(name);\n }\n if (/[^a-z0-9\\-#$%&'*+.^_`|~]/i.test(name)) {\n throw new TypeError('Invalid character in header field name')\n }\n return name.toLowerCase()\n }\n\n function normalizeValue(value) {\n if (typeof value !== 'string') {\n value = String(value);\n }\n return value\n }\n\n // Build a destructive iterator for the value list\n function iteratorFor(items) {\n var iterator = {\n next: function() {\n var value = items.shift();\n return {done: value === undefined, value: value}\n }\n };\n\n if (support.iterable) {\n iterator[Symbol.iterator] = function() {\n return iterator\n };\n }\n\n return iterator\n }\n\n function Headers(headers) {\n this.map = {};\n\n if (headers instanceof Headers) {\n headers.forEach(function(value, name) {\n this.append(name, value);\n }, this);\n } else if (Array.isArray(headers)) {\n headers.forEach(function(header) {\n this.append(header[0], header[1]);\n }, this);\n } else if (headers) {\n Object.getOwnPropertyNames(headers).forEach(function(name) {\n this.append(name, headers[name]);\n }, this);\n }\n }\n\n Headers.prototype.append = function(name, value) {\n name = normalizeName(name);\n value = normalizeValue(value);\n var oldValue = this.map[name];\n this.map[name] = oldValue ? oldValue + ', ' + value : value;\n };\n\n Headers.prototype['delete'] = function(name) {\n delete this.map[normalizeName(name)];\n };\n\n Headers.prototype.get = function(name) {\n name = normalizeName(name);\n return this.has(name) ? this.map[name] : null\n };\n\n Headers.prototype.has = function(name) {\n return this.map.hasOwnProperty(normalizeName(name))\n };\n\n Headers.prototype.set = function(name, value) {\n this.map[normalizeName(name)] = normalizeValue(value);\n };\n\n Headers.prototype.forEach = function(callback, thisArg) {\n for (var name in this.map) {\n if (this.map.hasOwnProperty(name)) {\n callback.call(thisArg, this.map[name], name, this);\n }\n }\n };\n\n Headers.prototype.keys = function() {\n var items = [];\n this.forEach(function(value, name) {\n items.push(name);\n });\n return iteratorFor(items)\n };\n\n Headers.prototype.values = function() {\n var items = [];\n this.forEach(function(value) {\n items.push(value);\n });\n return iteratorFor(items)\n };\n\n Headers.prototype.entries = function() {\n var items = [];\n this.forEach(function(value, name) {\n items.push([name, value]);\n });\n return iteratorFor(items)\n };\n\n if (support.iterable) {\n Headers.prototype[Symbol.iterator] = Headers.prototype.entries;\n }\n\n function consumed(body) {\n if (body.bodyUsed) {\n return Promise.reject(new TypeError('Already read'))\n }\n body.bodyUsed = true;\n }\n\n function fileReaderReady(reader) {\n return new Promise(function(resolve, reject) {\n reader.onload = function() {\n resolve(reader.result);\n };\n reader.onerror = function() {\n reject(reader.error);\n };\n })\n }\n\n function readBlobAsArrayBuffer(blob) {\n var reader = new FileReader();\n var promise = fileReaderReady(reader);\n reader.readAsArrayBuffer(blob);\n return promise\n }\n\n function readBlobAsText(blob) {\n var reader = new FileReader();\n var promise = fileReaderReady(reader);\n reader.readAsTLine truncated
|
||||||
"mappings": "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 truncated
|
"mappings": "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 truncated
|
||||||
"names": ["self", "exports", "fetch", "_b", "_a"]
|
"names": ["self", "exports", "fetch", "_b", "_a"]
|
||||||
}
|
}
|
||||||
+1
-1
@@ -256,7 +256,7 @@ import {
|
|||||||
weeksToDays,
|
weeksToDays,
|
||||||
yearsToMonths,
|
yearsToMonths,
|
||||||
yearsToQuarters
|
yearsToQuarters
|
||||||
} from "./chunk-FSI7PPCM.js";
|
} from "./chunk-SJKHQ62W.js";
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
export {
|
export {
|
||||||
add,
|
add,
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ import {
|
|||||||
requiredArgs,
|
requiredArgs,
|
||||||
startOfUTCWeek,
|
startOfUTCWeek,
|
||||||
toDate
|
toDate
|
||||||
} from "./chunk-FSI7PPCM.js";
|
} from "./chunk-SJKHQ62W.js";
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
|
|
||||||
// node_modules/date-fns/esm/locale/af/_lib/formatDistance/index.js
|
// node_modules/date-fns/esm/locale/af/_lib/formatDistance/index.js
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../../date-fns/esm/locale/af/_lib/formatDistance/index.js", "../../date-fns/esm/locale/af/_lib/formatLong/index.js", "../../date-fns/esm/locale/af/_lib/formatRelative/index.js", "../../date-fns/esm/locale/af/_lib/localize/index.js", "../../date-fns/esm/locale/af/_lib/match/index.js", "../../date-fns/esm/locale/af/index.js", "../../date-fns/esm/locale/ar/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar/_lib/localize/index.js", "../../date-fns/esm/locale/ar/_lib/match/index.js", "../../date-fns/esm/locale/ar/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/localize/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/match/index.js", "../../date-fns/esm/locale/ar-DZ/index.js", "../../date-fns/esm/locale/ar-EG/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-EG/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-EG/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-EG/_lib/localize/index.js", "../../date-fns/esm/locale/ar-EG/_lib/match/index.js", "../../date-fns/esm/locale/ar-EG/index.js", "../../date-fns/esm/locale/ar-MA/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-MA/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-MA/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-MA/_lib/localize/index.js", "../../date-fns/esm/locale/ar-MA/_lib/match/index.js", "../../date-fns/esm/locale/ar-MA/index.js", "../../date-fns/esm/locale/ar-SA/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-SA/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-SA/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-SA/_lib/localize/index.js", "../../date-fns/esm/locale/ar-SA/_lib/match/index.js", "../../date-fns/esm/locale/ar-SA/index.js", "../../date-fns/esm/locale/ar-TN/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-TN/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-TN/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-TN/_lib/localize/index.js", "../../date-fns/esm/locale/ar-TN/_lib/match/index.js", "../../date-fns/esm/locale/ar-TN/index.js", "../../date-fns/esm/locale/az/_lib/formatDistance/index.js", "../../date-fns/esm/locale/az/_lib/formatLong/index.js", "../../date-fns/esm/locale/az/_lib/formatRelative/index.js", "../../date-fns/esm/locale/az/_lib/localize/index.js", "../../date-fns/esm/locale/az/_lib/match/index.js", "../../date-fns/esm/locale/az/index.js", "../../date-fns/esm/locale/be/_lib/formatDistance/index.js", "../../date-fns/esm/locale/be/_lib/formatLong/index.js", "../../date-fns/esm/_lib/isSameUTCWeek/index.js", "../../date-fns/esm/locale/be/_lib/formatRelative/index.js", "../../date-fns/esm/locale/be/_lib/localize/index.js", "../../date-fns/esm/locale/be/_lib/match/index.js", "../../date-fns/esm/locale/be/index.js", "../../date-fns/esm/locale/be-tarask/_lib/formatDistance/index.js", "../../date-fns/esm/locale/be-tarask/_lib/formatLong/index.js", "../../date-fns/esm/locale/be-tarask/_lib/formatRelative/index.js", "../../date-fns/esm/locale/be-tarask/_lib/localize/index.js", "../../date-fns/esm/locale/be-tarask/_lib/match/index.js", "../../date-fns/esm/locale/be-tarask/index.js", "../../date-fns/esm/locale/bg/_lib/formatDistance/index.js", "../../date-fns/esm/locale/bg/_lib/formatLong/index.js", "../../date-fns/esm/locale/bg/_lib/formatRelative/index.js", "../../date-fns/esm/locale/bg/_lib/localize/index.js", "../../date-fns/esm/locale/bg/_lib/match/index.js", "../../date-fns/esm/locale/bg/index.js", "../../date-fns/esm/locale/bn/_lib/localize/index.js", "../../date-fns/esm/locale/bn/_lib/formatDistance/index.js", "../../date-fns/esm/locale/bn/_lib/formatLong/index.js", "../../date-fns/esm/locale/bn/_lib/formatRelative/index.js", "../../date-fns/esm/locale/bn/_lib/match/index.js", "../../date-fns/esm/locale/bn/index.js", "../../date-fns/esm/locale/bs/_lib/formatDistance/index.js", "../../date-fns/esm/locale/bs/_lib/formatLong/index.js", "../../date-fns/esm/locale/bs/_lib/formatRelative/index.js", "../../date-fns/esm/locale/bs/_lib/localize/index.js", "../../date-fns/esm/locale/bs/_lib/match/index.js", "../../date-fns/esm/locale/bs/index.js", "../../date-fns/esm/locale/ca/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ca/_lib/formatLong/index.js", "../../date-fns/esm/locale/ca/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ca/_lib/localize/index.js", "../../date-fns/esm/locale/ca/_lib/match/index.js", "../../date-fns/esm/locale/ca/index.js", "../../date-fns/esm/locale/cs/_lib/formatDistance/index.js", "../../date-fns/esm/locale/cs/_lib/formatLong/index.js", "../../date-fns/esm/locale/cs/_lib/formatRelative/index.js", "../../date-fns/esm/locale/cs/_lib/localize/inLine truncated
|
"sources": ["../../date-fns/esm/locale/af/_lib/formatDistance/index.js", "../../date-fns/esm/locale/af/_lib/formatLong/index.js", "../../date-fns/esm/locale/af/_lib/formatRelative/index.js", "../../date-fns/esm/locale/af/_lib/localize/index.js", "../../date-fns/esm/locale/af/_lib/match/index.js", "../../date-fns/esm/locale/af/index.js", "../../date-fns/esm/locale/ar/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar/_lib/localize/index.js", "../../date-fns/esm/locale/ar/_lib/match/index.js", "../../date-fns/esm/locale/ar/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/localize/index.js", "../../date-fns/esm/locale/ar-DZ/_lib/match/index.js", "../../date-fns/esm/locale/ar-DZ/index.js", "../../date-fns/esm/locale/ar-EG/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-EG/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-EG/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-EG/_lib/localize/index.js", "../../date-fns/esm/locale/ar-EG/_lib/match/index.js", "../../date-fns/esm/locale/ar-EG/index.js", "../../date-fns/esm/locale/ar-MA/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-MA/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-MA/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-MA/_lib/localize/index.js", "../../date-fns/esm/locale/ar-MA/_lib/match/index.js", "../../date-fns/esm/locale/ar-MA/index.js", "../../date-fns/esm/locale/ar-SA/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-SA/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-SA/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-SA/_lib/localize/index.js", "../../date-fns/esm/locale/ar-SA/_lib/match/index.js", "../../date-fns/esm/locale/ar-SA/index.js", "../../date-fns/esm/locale/ar-TN/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ar-TN/_lib/formatLong/index.js", "../../date-fns/esm/locale/ar-TN/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ar-TN/_lib/localize/index.js", "../../date-fns/esm/locale/ar-TN/_lib/match/index.js", "../../date-fns/esm/locale/ar-TN/index.js", "../../date-fns/esm/locale/az/_lib/formatDistance/index.js", "../../date-fns/esm/locale/az/_lib/formatLong/index.js", "../../date-fns/esm/locale/az/_lib/formatRelative/index.js", "../../date-fns/esm/locale/az/_lib/localize/index.js", "../../date-fns/esm/locale/az/_lib/match/index.js", "../../date-fns/esm/locale/az/index.js", "../../date-fns/esm/locale/be/_lib/formatDistance/index.js", "../../date-fns/esm/locale/be/_lib/formatLong/index.js", "../../date-fns/esm/_lib/isSameUTCWeek/index.js", "../../date-fns/esm/locale/be/_lib/formatRelative/index.js", "../../date-fns/esm/locale/be/_lib/localize/index.js", "../../date-fns/esm/locale/be/_lib/match/index.js", "../../date-fns/esm/locale/be/index.js", "../../date-fns/esm/locale/be-tarask/_lib/formatDistance/index.js", "../../date-fns/esm/locale/be-tarask/_lib/formatLong/index.js", "../../date-fns/esm/locale/be-tarask/_lib/formatRelative/index.js", "../../date-fns/esm/locale/be-tarask/_lib/localize/index.js", "../../date-fns/esm/locale/be-tarask/_lib/match/index.js", "../../date-fns/esm/locale/be-tarask/index.js", "../../date-fns/esm/locale/bg/_lib/formatDistance/index.js", "../../date-fns/esm/locale/bg/_lib/formatLong/index.js", "../../date-fns/esm/locale/bg/_lib/formatRelative/index.js", "../../date-fns/esm/locale/bg/_lib/localize/index.js", "../../date-fns/esm/locale/bg/_lib/match/index.js", "../../date-fns/esm/locale/bg/index.js", "../../date-fns/esm/locale/bn/_lib/localize/index.js", "../../date-fns/esm/locale/bn/_lib/formatDistance/index.js", "../../date-fns/esm/locale/bn/_lib/formatLong/index.js", "../../date-fns/esm/locale/bn/_lib/formatRelative/index.js", "../../date-fns/esm/locale/bn/_lib/match/index.js", "../../date-fns/esm/locale/bn/index.js", "../../date-fns/esm/locale/bs/_lib/formatDistance/index.js", "../../date-fns/esm/locale/bs/_lib/formatLong/index.js", "../../date-fns/esm/locale/bs/_lib/formatRelative/index.js", "../../date-fns/esm/locale/bs/_lib/localize/index.js", "../../date-fns/esm/locale/bs/_lib/match/index.js", "../../date-fns/esm/locale/bs/index.js", "../../date-fns/esm/locale/ca/_lib/formatDistance/index.js", "../../date-fns/esm/locale/ca/_lib/formatLong/index.js", "../../date-fns/esm/locale/ca/_lib/formatRelative/index.js", "../../date-fns/esm/locale/ca/_lib/localize/index.js", "../../date-fns/esm/locale/ca/_lib/match/index.js", "../../date-fns/esm/locale/ca/index.js", "../../date-fns/esm/locale/cs/_lib/formatDistance/index.js", "../../date-fns/esm/locale/cs/_lib/formatLong/index.js", "../../date-fns/esm/locale/cs/_lib/formatRelative/index.js", "../../date-fns/esm/locale/cs/_lib/localize/inLine truncated
|
||||||
"sourcesContent": ["var formatDistanceLocale = {\r\n lessThanXSeconds: {\r\n one: \"minder as 'n sekonde\",\r\n other: 'minder as {{count}} sekondes'\r\n },\r\n xSeconds: {\r\n one: '1 sekonde',\r\n other: '{{count}} sekondes'\r\n },\r\n halfAMinute: \"'n halwe minuut\",\r\n lessThanXMinutes: {\r\n one: \"minder as 'n minuut\",\r\n other: 'minder as {{count}} minute'\r\n },\r\n xMinutes: {\r\n one: \"'n minuut\",\r\n other: '{{count}} minute'\r\n },\r\n aboutXHours: {\r\n one: 'ongeveer 1 uur',\r\n other: 'ongeveer {{count}} ure'\r\n },\r\n xHours: {\r\n one: '1 uur',\r\n other: '{{count}} ure'\r\n },\r\n xDays: {\r\n one: '1 dag',\r\n other: '{{count}} dae'\r\n },\r\n aboutXWeeks: {\r\n one: 'ongeveer 1 week',\r\n other: 'ongeveer {{count}} weke'\r\n },\r\n xWeeks: {\r\n one: '1 week',\r\n other: '{{count}} weke'\r\n },\r\n aboutXMonths: {\r\n one: 'ongeveer 1 maand',\r\n other: 'ongeveer {{count}} maande'\r\n },\r\n xMonths: {\r\n one: '1 maand',\r\n other: '{{count}} maande'\r\n },\r\n aboutXYears: {\r\n one: 'ongeveer 1 jaar',\r\n other: 'ongeveer {{count}} jaar'\r\n },\r\n xYears: {\r\n one: '1 jaar',\r\n other: '{{count}} jaar'\r\n },\r\n overXYears: {\r\n one: 'meer as 1 jaar',\r\n other: 'meer as {{count}} jaar'\r\n },\r\n almostXYears: {\r\n one: 'byna 1 jaar',\r\n other: 'byna {{count}} jaar'\r\n }\r\n};\r\nvar formatDistance = function formatDistance(token, count, options) {\r\n var result;\r\n var tokenValue = formatDistanceLocale[token];\r\n if (typeof tokenValue === 'string') {\r\n result = tokenValue;\r\n } else if (count === 1) {\r\n result = tokenValue.one;\r\n } else {\r\n result = tokenValue.other.replace('{{count}}', String(count));\r\n }\r\n if (options !== null && options !== void 0 && options.addSuffix) {\r\n if (options.comparison && options.comparison > 0) {\r\n return 'oor ' + result;\r\n } else {\r\n return result + ' gelede';\r\n }\r\n }\r\n return result;\r\n};\r\nexport default formatDistance;", "import buildFormatLongFn from \"../../../_lib/buildFormatLongFn/index.js\";\r\nvar dateFormats = {\r\n full: 'EEEE, d MMMM yyyy',\r\n long: 'd MMMM yyyy',\r\n medium: 'd MMM yyyy',\r\n short: 'yyyy/MM/dd'\r\n};\r\nvar timeFormats = {\r\n full: 'HH:mm:ss zzzz',\r\n long: 'HH:mm:ss z',\r\n medium: 'HH:mm:ss',\r\n short: 'HH:mm'\r\n};\r\nvar dateTimeFormats = {\r\n full: \"{{date}} 'om' {{time}}\",\r\n long: \"{{date}} 'om' {{time}}\",\r\n medium: '{{date}}, {{time}}',\r\n short: '{{date}}, {{time}}'\r\n};\r\nvar formatLong = {\r\n date: buildFormatLongFn({\r\n formats: dateFormats,\r\n defaultWidth: 'full'\r\n }),\r\n time: buildFormatLongFn({\r\n formats: timeFormats,\r\n defaultWidth: 'full'\r\n }),\r\n dateTime: buildFormatLongFn({\r\n formats: dateTimeFormats,\r\n defaultWidth: 'full'\r\n })\r\n};\r\nexport default formatLong;", "var formatRelativeLocale = {\r\n lastWeek: \"'verlede' eeee 'om' p\",\r\n yesterday: \"'gister om' p\",\r\n today: \"'vandag om' p\",\r\n tomorrow: \"'môre om' p\",\r\n nextWeek: \"eeee 'om' p\",\r\n other: 'P'\r\n};\r\nvar formatRelative = function formatRelative(token, _date, _baseDate, _options) {\r\n return formatRelativeLocale[token];\r\n};\r\nexport default formatRelative;", "import buildLocalizeFn from \"../../../_lib/buildLocalizeFn/index.js\";\r\nvar eraValues = {\r\n narrow: ['vC', 'nC'],\r\n abbreviated: ['vC', 'nC'],\r\n wide: ['voor Christus', 'na Christus']\r\n};\r\nvar quarterValues = {\r\n narrow: ['1', '2', '3', '4'],\r\n abbreviated: ['K1', 'K2', 'K3', 'K4'],\r\n wide: ['1ste kwartaal', '2de kwartaal', '3de kwartaal', '4de kwartaal']\r\n};\r\nvar monthValues = {\r\n narrow: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],\r\n abbreviated: ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],\r\n wide: ['Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', 'Augustus', 'September', 'Oktober', 'November', 'Desember']\r\n};\r\nvar dayValues = {\r\n narrow: ['S', 'M', 'D', 'W', 'D', 'V', 'S'],\r\n short: ['So', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Sa'],\r\n abbreviated: ['Son', 'Maa', 'Din', 'Woe', 'Don', 'Vry', 'Sat'],\r\n wide: ['Sondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag']\r\n};\r\nvar dayPeriodValues = {\r\n narrow: {\r\n am: 'vm',\r\n pm: 'nm',\r\n midnight: 'middernag',\r\n noon: 'middaguur',\r\n morning: 'oggend',\r\n afternoon: 'middag',\r\n evening: 'laat middag',\r\n night: 'aand'\r\n },\r\n abbreviated: {\r\n am: 'vm',\r\n pm: 'nm',\r\n midnight: 'middernag',\r\n noon: 'middaguur',\r\n morning: 'oggend',\r\n afternoon: 'middag',\r\n evening: 'laat middag',\r\n night: 'aand'\r\n },\r\n wide: {\r\n am: 'vm',\r\n pm: 'nm',\r\n midnight: 'middernag',\r\n noon: 'middaguur',\r\nLine truncated
|
"sourcesContent": ["var formatDistanceLocale = {\n lessThanXSeconds: {\n one: \"minder as 'n sekonde\",\n other: 'minder as {{count}} sekondes'\n },\n xSeconds: {\n one: '1 sekonde',\n other: '{{count}} sekondes'\n },\n halfAMinute: \"'n halwe minuut\",\n lessThanXMinutes: {\n one: \"minder as 'n minuut\",\n other: 'minder as {{count}} minute'\n },\n xMinutes: {\n one: \"'n minuut\",\n other: '{{count}} minute'\n },\n aboutXHours: {\n one: 'ongeveer 1 uur',\n other: 'ongeveer {{count}} ure'\n },\n xHours: {\n one: '1 uur',\n other: '{{count}} ure'\n },\n xDays: {\n one: '1 dag',\n other: '{{count}} dae'\n },\n aboutXWeeks: {\n one: 'ongeveer 1 week',\n other: 'ongeveer {{count}} weke'\n },\n xWeeks: {\n one: '1 week',\n other: '{{count}} weke'\n },\n aboutXMonths: {\n one: 'ongeveer 1 maand',\n other: 'ongeveer {{count}} maande'\n },\n xMonths: {\n one: '1 maand',\n other: '{{count}} maande'\n },\n aboutXYears: {\n one: 'ongeveer 1 jaar',\n other: 'ongeveer {{count}} jaar'\n },\n xYears: {\n one: '1 jaar',\n other: '{{count}} jaar'\n },\n overXYears: {\n one: 'meer as 1 jaar',\n other: 'meer as {{count}} jaar'\n },\n almostXYears: {\n one: 'byna 1 jaar',\n other: 'byna {{count}} jaar'\n }\n};\nvar formatDistance = function formatDistance(token, count, options) {\n var result;\n var tokenValue = formatDistanceLocale[token];\n if (typeof tokenValue === 'string') {\n result = tokenValue;\n } else if (count === 1) {\n result = tokenValue.one;\n } else {\n result = tokenValue.other.replace('{{count}}', String(count));\n }\n if (options !== null && options !== void 0 && options.addSuffix) {\n if (options.comparison && options.comparison > 0) {\n return 'oor ' + result;\n } else {\n return result + ' gelede';\n }\n }\n return result;\n};\nexport default formatDistance;", "import buildFormatLongFn from \"../../../_lib/buildFormatLongFn/index.js\";\nvar dateFormats = {\n full: 'EEEE, d MMMM yyyy',\n long: 'd MMMM yyyy',\n medium: 'd MMM yyyy',\n short: 'yyyy/MM/dd'\n};\nvar timeFormats = {\n full: 'HH:mm:ss zzzz',\n long: 'HH:mm:ss z',\n medium: 'HH:mm:ss',\n short: 'HH:mm'\n};\nvar dateTimeFormats = {\n full: \"{{date}} 'om' {{time}}\",\n long: \"{{date}} 'om' {{time}}\",\n medium: '{{date}}, {{time}}',\n short: '{{date}}, {{time}}'\n};\nvar formatLong = {\n date: buildFormatLongFn({\n formats: dateFormats,\n defaultWidth: 'full'\n }),\n time: buildFormatLongFn({\n formats: timeFormats,\n defaultWidth: 'full'\n }),\n dateTime: buildFormatLongFn({\n formats: dateTimeFormats,\n defaultWidth: 'full'\n })\n};\nexport default formatLong;", "var formatRelativeLocale = {\n lastWeek: \"'verlede' eeee 'om' p\",\n yesterday: \"'gister om' p\",\n today: \"'vandag om' p\",\n tomorrow: \"'môre om' p\",\n nextWeek: \"eeee 'om' p\",\n other: 'P'\n};\nvar formatRelative = function formatRelative(token, _date, _baseDate, _options) {\n return formatRelativeLocale[token];\n};\nexport default formatRelative;", "import buildLocalizeFn from \"../../../_lib/buildLocalizeFn/index.js\";\nvar eraValues = {\n narrow: ['vC', 'nC'],\n abbreviated: ['vC', 'nC'],\n wide: ['voor Christus', 'na Christus']\n};\nvar quarterValues = {\n narrow: ['1', '2', '3', '4'],\n abbreviated: ['K1', 'K2', 'K3', 'K4'],\n wide: ['1ste kwartaal', '2de kwartaal', '3de kwartaal', '4de kwartaal']\n};\nvar monthValues = {\n narrow: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],\n abbreviated: ['Jan', 'Feb', 'Mrt', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],\n wide: ['Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', 'Augustus', 'September', 'Oktober', 'November', 'Desember']\n};\nvar dayValues = {\n narrow: ['S', 'M', 'D', 'W', 'D', 'V', 'S'],\n short: ['So', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Sa'],\n abbreviated: ['Son', 'Maa', 'Din', 'Woe', 'Don', 'Vry', 'Sat'],\n wide: ['Sondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag']\n};\nvar dayPeriodValues = {\n narrow: {\n am: 'vm',\n pm: 'nm',\n midnight: 'middernag',\n noon: 'middaguur',\n morning: 'oggend',\n afternoon: 'middag',\n evening: 'laat middag',\n night: 'aand'\n },\n abbreviated: {\n am: 'vm',\n pm: 'nm',\n midnight: 'middernag',\n noon: 'middaguur',\n morning: 'oggend',\n afternoon: 'middag',\n evening: 'laat middag',\n night: 'aand'\n },\n wide: {\n am: 'vm',\n pm: 'nm',\n midnight: 'middernag',\n noon: 'middaguur',\n morning: 'oggend',\n afternoon: 'middag',\n evening: 'laat middag',\n night: 'aand'\n }\n};\nvar formattingDayPeriodValues = {\n narrow: {\n am: 'vm',\n pm: 'nm',\n midnight: 'middernag',\n noon: 'uur die middag',\n morning: 'uur die oggend',\n afternoon: 'uur die middag',\n evening: 'uur die aand',\n night:Line truncated
|
||||||
"mappings": "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 truncated
|
"mappings": "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 truncated
|
||||||
"names": ["formatDistance", "formatDistance_default", "formatRelative", "formatRelative_default", "ordinalNumber", "localize_default", "valueCallback", "match_default", "formatDistance_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuartLine truncated
|
"names": ["formatDistance", "formatDistance_default", "formatRelative", "formatRelative_default", "ordinalNumber", "localize_default", "valueCallback", "match_default", "formatDistance_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuarterPatterns", "matchMonthPatterns", "parseMonthPatterns", "matchDayPatterns", "parseDayPatterns", "matchDayPeriodPatterns", "parseDayPeriodPatterns", "match", "valueCallback", "match_default", "locale", "formatDistance_default", "formatLong_default", "formatRelative_default", "localize_default", "match_default", "formatDistanceLocale", "formatDistance", "formatDistance_default", "dateFormats", "timeFormats", "dateTimeFormats", "formatLong", "formatLong_default", "formatRelativeLocale", "formatRelative", "formatRelative_default", "eraValues", "quarterValues", "monthValues", "dayValues", "dayPeriodValues", "formattingDayPeriodValues", "ordinalNumber", "localize", "argumentCallback", "localize_default", "matchOrdinalNumberPattern", "parseOrdinalNumberPattern", "matchEraPatterns", "parseEraPatterns", "matchQuarterPatterns", "parseQuartLine truncated
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
require_react_dom
|
require_react_dom
|
||||||
} from "./chunk-TDH2IRYZ.js";
|
} from "./chunk-PJEEZAML.js";
|
||||||
import "./chunk-QRULMDK5.js";
|
import "./chunk-DRWLMN53.js";
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
export default require_react_dom();
|
export default require_react_dom();
|
||||||
//# sourceMappingURL=react-dom.js.map
|
//# sourceMappingURL=react-dom.js.map
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
require_react_dom
|
require_react_dom
|
||||||
} from "./chunk-TDH2IRYZ.js";
|
} from "./chunk-PJEEZAML.js";
|
||||||
import "./chunk-QRULMDK5.js";
|
import "./chunk-DRWLMN53.js";
|
||||||
import {
|
import {
|
||||||
__commonJS
|
__commonJS
|
||||||
} from "./chunk-G3PMV62Z.js";
|
} from "./chunk-G3PMV62Z.js";
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../../react-dom/client.js"],
|
"sources": ["../../react-dom/client.js"],
|
||||||
"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"],
|
"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"],
|
||||||
"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;",
|
"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": []
|
"names": []
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
GenIcon
|
GenIcon
|
||||||
} from "./chunk-7VTGDDTZ.js";
|
} from "./chunk-DQJKJRV5.js";
|
||||||
import "./chunk-QRULMDK5.js";
|
import "./chunk-DRWLMN53.js";
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
|
|
||||||
// node_modules/react-icons/fa/index.esm.js
|
// node_modules/react-icons/fa/index.esm.js
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../../react-icons/fa/index.esm.js"],
|
"sources": ["../../react-icons/fa/index.esm.js"],
|
||||||
"sourcesContent": ["// THIS FILE IS AUTO GENERATED\r\nimport { GenIcon } from '../lib';\r\nexport function Fa500Px (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M103.3 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2h.3v-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5s-68.8 13.8-93.5 38.5c-.3.3-16 16.5-21.2 23.9l-.5.6c-3.3 4.7-6.3 9.1-20.1 6.1-6.9-1.7-14.3-5.8-14.3-11.8V20c0-5 3.9-10.5 10.5-10.5h241.3c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1H130.3v132.9h.3c104.2-109.8 282.8-36 282.8 108.9 0 178.1-244.8 220.3-310.1 62.8zm63.3-260.8c-.5 4.2 4.6 24.5 14.6 20.6C306 56.6 384 144.5 390.6 144.5c4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2zM393 414.7C283 524.6 94 475.5 61 310.5c0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4zM213.6 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6.2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2L265.6 284l18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L249.7 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L234 284c-18.1 17.9-20.4 19.2-20.4 22.6z\"}}]})(props);\r\n};\r\nexport function FaAccessibleIcon (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46c-30 26.8-70.6-18.5-40.5-45.4l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6L285.7 229l106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zm-64.9-154c28.1 0 50.9-22.8 50.9-50.9C409.9 22.8 387.1 0 359 0c-28.1 0-50.9 22.8-50.9 50.9 0 28.1 22.8 50.9 50.9 50.9zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1l-39.7-39.7C36.4 287 24 320.3 24 356.4c0 130.7 150.7 201.4 251.4 122.5l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z\"}}]})(props);\r\n};\r\nexport function FaAccusoft (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 640 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M322.1 252v-1l-51.2-65.8s-12 1.6-25 15.1c-9 9.3-242.1 239.1-243.4 240.9-7 10 1.6 6.8 15.7 1.7.8 0 114.5-36.6 114.5-36.6.5-.6-.1-.1.6-.6-.4-5.1-.8-26.2-1-27.7-.6-5.2 2.2-6.9 7-8.9l92.6-33.8c.6-.8 88.5-81.7 90.2-83.3zm160.1 120.1c13.3 16.1 20.7 13.3 30.8 9.3 3.2-1.2 115.4-47.6 117.8-48.9 8-4.3-1.7-16.7-7.2-23.4-2.1-2.5-205.1-245.6-207.2-248.3-9.7-12.2-14.3-12.9-38.4-12.8-10.2 0-106.8.5-116.5.6-19.2.1-32.9-.3-19.2 16.9C250 75 476.5 365.2 482.2 372.1zm152.7 1.6c-2.3-.3-24.6-4.7-38-7.2 0 0-115 50.4-117.5 51.6-16 7.3-26.9-3.2-36.7-14.6l-57.1-74c-5.4-.9-60.4-9.6-65.3-9.3-3.1.2-9.6.8-14.4 2.9-4.9 2.1-145.2 52.8-150.2 54.7-5.1 2-11.4 3.6-11.1 7.6.2 2.5 2 2.6 4.6 3.5 2.7.8 300.9 67.6 308 69.1 15.6 3.3 38.5 10.5 53.6 1.7 2.1-1.2 123.8-76.4 125.8-77.8 5.4-4 4.3-6.8-1.7-8.2z\"}}]})(props);\r\n};\r\nexport function FaAcquisitionsIncorporated (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 384 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M357.45 468.2c-1.2-7.7-1.3-7.6-9.6-7.6-99.8.2-111.8-2.4-112.7-2.6-12.3-1.7-20.6-10.5-21-23.1-.1-1.6-.2-71.6-1-129.1-.1-4.7 1.6-6.4 5.9-7.5 12.5-3 24.9-6.1 37.3-9.7 4.3-1.3 6.8-.2 8.4 3.5 4.5 10.3 8.8 20.6 13.2 30.9 1.6 3.7.1 4.4-3.4 4.4-10-.2-20-.1-30.4-.1v27h116c-1.4-9.5-2.7-18.1-4-27.5-7 0-13.8.4-20.4-.1-22.6-1.6-18.3-4.4-84-158.6-8.8-20.1-27.9-62.1-36.5-89.2-4.4-14 5.5-25.4 18.9-26.6 18.6-1.7 37.5-1.6 56.2-2 20.6-.4 41.2-.4 61.8-.5 3.1 0 4-1.4 4.3-4.3 1.2-9.8 2.7-19.5 4-29.2.8-5.3 1.6-10.7 2.4-16.1L23.75 0c-3.6 0-5.3 1.1-4.6 5.3 2.2 13.2-.8.8 6.4 45.3 63.4 0 71.8.9 101.8.5 12.3-.2 37 3.5 37.7 22.1.4 11.4-1.1 11.3-32.6 87.4-53.8 129.8-50.7 120.3-67.3 161-1.7 4.1-3.6 5.2-7.6 5.2-8.5-.2-17-.3-25.4.1-1.9.1-5.2 1.8-5.5 3.2-1.5 8.1-2.2 16.3-3.2 24.9h114.3v-27.6c-6.9 0-33.5.4-35.3-2.9 5.3-12.3 10.4-24.4 15.7-36.7 16.3 4 31.9 7.8 47.6 11.7 3.4.9 4.6 3 4.6 6.8-.1 42.9.1 85.9.2 128.8 0 10.2-5.5 19.1-14.9 23.1-6.5 2.7-3.3 3.4-121.4 2.4-5.3 0-7.1 2-7.6 6.8-1.5 12.9-2.9 25.9-5 38.8-.8 5 1.3 5.7 5.3 5.7 183.2.6-30.7 0 337.1 0-2.5-15-4.4-29.4-6.6-43.7zm-174.9-205.7c-13.3-4.2-26.6-8.2-39.9-12.5a44.53 44.53 0 0 1-5.8-2.9c17.2-44.3 34.2-88.1 51.3-132.1 7.5 2.4 7.9-.8 9.4 0 9.3 22.5 18.1 60.1 27 82.8 6.6 16.7 13 33.5 19.7 50.9a35.78 35.78 0 0 1-3.9 2.1c-13.1 3.9-26.4 7.5-39.4 11.7a27.66 27.66 0 0 1-18.4 0z\"}}]})(props);\r\n};\r\nexport function FaAdn (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 496 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M248 167.5l64.9 98.8H183.1l64.9-98.8zM496 256c0 1Line truncated
|
"sourcesContent": ["// THIS FILE IS AUTO GENERATED\nimport { GenIcon } from '../lib';\nexport function Fa500Px (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M103.3 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2h.3v-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5s-68.8 13.8-93.5 38.5c-.3.3-16 16.5-21.2 23.9l-.5.6c-3.3 4.7-6.3 9.1-20.1 6.1-6.9-1.7-14.3-5.8-14.3-11.8V20c0-5 3.9-10.5 10.5-10.5h241.3c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1H130.3v132.9h.3c104.2-109.8 282.8-36 282.8 108.9 0 178.1-244.8 220.3-310.1 62.8zm63.3-260.8c-.5 4.2 4.6 24.5 14.6 20.6C306 56.6 384 144.5 390.6 144.5c4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2zM393 414.7C283 524.6 94 475.5 61 310.5c0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4zM213.6 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6.2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2L265.6 284l18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L249.7 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L234 284c-18.1 17.9-20.4 19.2-20.4 22.6z\"}}]})(props);\n};\nexport function FaAccessibleIcon (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46c-30 26.8-70.6-18.5-40.5-45.4l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6L285.7 229l106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zm-64.9-154c28.1 0 50.9-22.8 50.9-50.9C409.9 22.8 387.1 0 359 0c-28.1 0-50.9 22.8-50.9 50.9 0 28.1 22.8 50.9 50.9 50.9zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1l-39.7-39.7C36.4 287 24 320.3 24 356.4c0 130.7 150.7 201.4 251.4 122.5l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z\"}}]})(props);\n};\nexport function FaAccusoft (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 640 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M322.1 252v-1l-51.2-65.8s-12 1.6-25 15.1c-9 9.3-242.1 239.1-243.4 240.9-7 10 1.6 6.8 15.7 1.7.8 0 114.5-36.6 114.5-36.6.5-.6-.1-.1.6-.6-.4-5.1-.8-26.2-1-27.7-.6-5.2 2.2-6.9 7-8.9l92.6-33.8c.6-.8 88.5-81.7 90.2-83.3zm160.1 120.1c13.3 16.1 20.7 13.3 30.8 9.3 3.2-1.2 115.4-47.6 117.8-48.9 8-4.3-1.7-16.7-7.2-23.4-2.1-2.5-205.1-245.6-207.2-248.3-9.7-12.2-14.3-12.9-38.4-12.8-10.2 0-106.8.5-116.5.6-19.2.1-32.9-.3-19.2 16.9C250 75 476.5 365.2 482.2 372.1zm152.7 1.6c-2.3-.3-24.6-4.7-38-7.2 0 0-115 50.4-117.5 51.6-16 7.3-26.9-3.2-36.7-14.6l-57.1-74c-5.4-.9-60.4-9.6-65.3-9.3-3.1.2-9.6.8-14.4 2.9-4.9 2.1-145.2 52.8-150.2 54.7-5.1 2-11.4 3.6-11.1 7.6.2 2.5 2 2.6 4.6 3.5 2.7.8 300.9 67.6 308 69.1 15.6 3.3 38.5 10.5 53.6 1.7 2.1-1.2 123.8-76.4 125.8-77.8 5.4-4 4.3-6.8-1.7-8.2z\"}}]})(props);\n};\nexport function FaAcquisitionsIncorporated (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 384 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M357.45 468.2c-1.2-7.7-1.3-7.6-9.6-7.6-99.8.2-111.8-2.4-112.7-2.6-12.3-1.7-20.6-10.5-21-23.1-.1-1.6-.2-71.6-1-129.1-.1-4.7 1.6-6.4 5.9-7.5 12.5-3 24.9-6.1 37.3-9.7 4.3-1.3 6.8-.2 8.4 3.5 4.5 10.3 8.8 20.6 13.2 30.9 1.6 3.7.1 4.4-3.4 4.4-10-.2-20-.1-30.4-.1v27h116c-1.4-9.5-2.7-18.1-4-27.5-7 0-13.8.4-20.4-.1-22.6-1.6-18.3-4.4-84-158.6-8.8-20.1-27.9-62.1-36.5-89.2-4.4-14 5.5-25.4 18.9-26.6 18.6-1.7 37.5-1.6 56.2-2 20.6-.4 41.2-.4 61.8-.5 3.1 0 4-1.4 4.3-4.3 1.2-9.8 2.7-19.5 4-29.2.8-5.3 1.6-10.7 2.4-16.1L23.75 0c-3.6 0-5.3 1.1-4.6 5.3 2.2 13.2-.8.8 6.4 45.3 63.4 0 71.8.9 101.8.5 12.3-.2 37 3.5 37.7 22.1.4 11.4-1.1 11.3-32.6 87.4-53.8 129.8-50.7 120.3-67.3 161-1.7 4.1-3.6 5.2-7.6 5.2-8.5-.2-17-.3-25.4.1-1.9.1-5.2 1.8-5.5 3.2-1.5 8.1-2.2 16.3-3.2 24.9h114.3v-27.6c-6.9 0-33.5.4-35.3-2.9 5.3-12.3 10.4-24.4 15.7-36.7 16.3 4 31.9 7.8 47.6 11.7 3.4.9 4.6 3 4.6 6.8-.1 42.9.1 85.9.2 128.8 0 10.2-5.5 19.1-14.9 23.1-6.5 2.7-3.3 3.4-121.4 2.4-5.3 0-7.1 2-7.6 6.8-1.5 12.9-2.9 25.9-5 38.8-.8 5 1.3 5.7 5.3 5.7 183.2.6-30.7 0 337.1 0-2.5-15-4.4-29.4-6.6-43.7zm-174.9-205.7c-13.3-4.2-26.6-8.2-39.9-12.5a44.53 44.53 0 0 1-5.8-2.9c17.2-44.3 34.2-88.1 51.3-132.1 7.5 2.4 7.9-.8 9.4 0 9.3 22.5 18.1 60.1 27 82.8 6.6 16.7 13 33.5 19.7 50.9a35.78 35.78 0 0 1-3.9 2.1c-13.1 3.9-26.4 7.5-39.4 11.7a27.66 27.66 0 0 1-18.4 0z\"}}]})(props);\n};\nexport function FaAdn (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 496 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M248 167.5l64.9 98.8H183.1l64.9-98.8zM496 256c0 136.9-111.1 248-248 248S0 392.9Line truncated
|
||||||
"mappings": "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 truncated
|
"mappings": "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 truncated
|
||||||
"names": []
|
"names": []
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
GenIcon
|
GenIcon
|
||||||
} from "./chunk-7VTGDDTZ.js";
|
} from "./chunk-DQJKJRV5.js";
|
||||||
import "./chunk-QRULMDK5.js";
|
import "./chunk-DRWLMN53.js";
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
|
|
||||||
// node_modules/react-icons/fa6/index.esm.js
|
// node_modules/react-icons/fa6/index.esm.js
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../../react-icons/fa6/index.esm.js"],
|
"sources": ["../../react-icons/fa6/index.esm.js"],
|
||||||
"sourcesContent": ["// THIS FILE IS AUTO GENERATED\r\nimport { GenIcon } from '../lib';\r\nexport function Fa42Group (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 640 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M320 96V416C341.011 416 361.818 411.861 381.23 403.821C400.641 395.78 418.28 383.995 433.138 369.138C447.995 354.28 459.78 336.641 467.821 317.23C475.861 297.818 480 277.011 480 256C480 234.989 475.861 214.182 467.821 194.771C459.78 175.359 447.995 157.72 433.138 142.863C418.28 128.005 400.641 116.22 381.23 108.179C361.818 100.139 341.011 96 320 96ZM0 256L160.002 416L320.003 256L160.002 96L0 256ZM480 256C480 277.011 484.138 297.818 492.179 317.23C500.219 336.643 512.005 354.28 526.862 369.138C541.72 383.995 559.357 395.781 578.77 403.821C598.182 411.862 618.989 416 640 416V96C597.565 96 556.869 112.858 526.862 142.863C496.857 172.869 480 213.565 480 256Z\"}}]})(props);\r\n};\r\nexport function Fa500Px (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M103.3 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2h.3v-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5s-68.8 13.8-93.5 38.5c-.3.3-16 16.5-21.2 23.9l-.5.6c-3.3 4.7-6.3 9.1-20.1 6.1-6.9-1.7-14.3-5.8-14.3-11.8V20c0-5 3.9-10.5 10.5-10.5h241.3c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1H130.3v132.9h.3c104.2-109.8 282.8-36 282.8 108.9 0 178.1-244.8 220.3-310.1 62.8zm63.3-260.8c-.5 4.2 4.6 24.5 14.6 20.6C306 56.6 384 144.5 390.6 144.5c4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2zM393 414.7C283 524.6 94 475.5 61 310.5c0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4zM213.6 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6.2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2L265.6 284l18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L249.7 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L234 284c-18.1 17.9-20.4 19.2-20.4 22.6z\"}}]})(props);\r\n};\r\nexport function FaAccessibleIcon (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46c-30 26.8-70.6-18.5-40.5-45.4l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6L285.7 229l106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zm-64.9-154c28.1 0 50.9-22.8 50.9-50.9C409.9 22.8 387.1 0 359 0c-28.1 0-50.9 22.8-50.9 50.9 0 28.1 22.8 50.9 50.9 50.9zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1l-39.7-39.7C36.4 287 24 320.3 24 356.4c0 130.7 150.7 201.4 251.4 122.5l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z\"}}]})(props);\r\n};\r\nexport function FaAccusoft (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 640 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M322.1 252v-1l-51.2-65.8s-12 1.6-25 15.1c-9 9.3-242.1 239.1-243.4 240.9-7 10 1.6 6.8 15.7 1.7.8 0 114.5-36.6 114.5-36.6.5-.6-.1-.1.6-.6-.4-5.1-.8-26.2-1-27.7-.6-5.2 2.2-6.9 7-8.9l92.6-33.8c.6-.8 88.5-81.7 90.2-83.3zm160.1 120.1c13.3 16.1 20.7 13.3 30.8 9.3 3.2-1.2 115.4-47.6 117.8-48.9 8-4.3-1.7-16.7-7.2-23.4-2.1-2.5-205.1-245.6-207.2-248.3-9.7-12.2-14.3-12.9-38.4-12.8-10.2 0-106.8.5-116.5.6-19.2.1-32.9-.3-19.2 16.9C250 75 476.5 365.2 482.2 372.1zm152.7 1.6c-2.3-.3-24.6-4.7-38-7.2 0 0-115 50.4-117.5 51.6-16 7.3-26.9-3.2-36.7-14.6l-57.1-74c-5.4-.9-60.4-9.6-65.3-9.3-3.1.2-9.6.8-14.4 2.9-4.9 2.1-145.2 52.8-150.2 54.7-5.1 2-11.4 3.6-11.1 7.6.2 2.5 2 2.6 4.6 3.5 2.7.8 300.9 67.6 308 69.1 15.6 3.3 38.5 10.5 53.6 1.7 2.1-1.2 123.8-76.4 125.8-77.8 5.4-4 4.3-6.8-1.7-8.2z\"}}]})(props);\r\n};\r\nexport function FaAdn (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 496 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M248 167.5l64.9 98.8H183.1l64.9-98.8zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-99.8 82.7L248 115.5 99.8 338.7h30.4l33.6-51.7h168.6l33.6 51.7h30.2z\"}}]})(props);\r\n};\r\nexport function FaAdversal (props) {\r\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 512 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M482.1 32H28.7C5.8 32 0 37.9 0 60.9v390.2C0 474.4 5.8 480 28.7 480h453.4c24.4 0 29.9-5.2 29.9-29.7V62.2c0-24.6-5.4-30.2-29.9-30.2zM178.4 220.3c-27.5-20.2-72.1-8.7-84.2 23.4-4.3 11.1-9.3 9.5-17.5 8.3-9.7-1.5-17.2-3.2-22.5-5.5-28.8-11.4 8.6-55.3 24.9-64.3 41.1-21.4 83.4-22.2 125.3-4.8 40.9 16.8 34.5 59.2 34.5 128.5 2.7 25.8-4.3 58.3 9.3 88.8 1Line truncated
|
"sourcesContent": ["// THIS FILE IS AUTO GENERATED\nimport { GenIcon } from '../lib';\nexport function Fa42Group (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 640 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M320 96V416C341.011 416 361.818 411.861 381.23 403.821C400.641 395.78 418.28 383.995 433.138 369.138C447.995 354.28 459.78 336.641 467.821 317.23C475.861 297.818 480 277.011 480 256C480 234.989 475.861 214.182 467.821 194.771C459.78 175.359 447.995 157.72 433.138 142.863C418.28 128.005 400.641 116.22 381.23 108.179C361.818 100.139 341.011 96 320 96ZM0 256L160.002 416L320.003 256L160.002 96L0 256ZM480 256C480 277.011 484.138 297.818 492.179 317.23C500.219 336.643 512.005 354.28 526.862 369.138C541.72 383.995 559.357 395.781 578.77 403.821C598.182 411.862 618.989 416 640 416V96C597.565 96 556.869 112.858 526.862 142.863C496.857 172.869 480 213.565 480 256Z\"}}]})(props);\n};\nexport function Fa500Px (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M103.3 344.3c-6.5-14.2-6.9-18.3 7.4-23.1 25.6-8 8 9.2 43.2 49.2h.3v-93.9c1.2-50.2 44-92.2 97.7-92.2 53.9 0 97.7 43.5 97.7 96.8 0 63.4-60.8 113.2-128.5 93.3-10.5-4.2-2.1-31.7 8.5-28.6 53 0 89.4-10.1 89.4-64.4 0-61-77.1-89.6-116.9-44.6-23.5 26.4-17.6 42.1-17.6 157.6 50.7 31 118.3 22 160.4-20.1 24.8-24.8 38.5-58 38.5-93 0-35.2-13.8-68.2-38.8-93.3-24.8-24.8-57.8-38.5-93.3-38.5s-68.8 13.8-93.5 38.5c-.3.3-16 16.5-21.2 23.9l-.5.6c-3.3 4.7-6.3 9.1-20.1 6.1-6.9-1.7-14.3-5.8-14.3-11.8V20c0-5 3.9-10.5 10.5-10.5h241.3c8.3 0 8.3 11.6 8.3 15.1 0 3.9 0 15.1-8.3 15.1H130.3v132.9h.3c104.2-109.8 282.8-36 282.8 108.9 0 178.1-244.8 220.3-310.1 62.8zm63.3-260.8c-.5 4.2 4.6 24.5 14.6 20.6C306 56.6 384 144.5 390.6 144.5c4.8 0 22.8-15.3 14.3-22.8-93.2-89-234.5-57-238.3-38.2zM393 414.7C283 524.6 94 475.5 61 310.5c0-12.2-30.4-7.4-28.9 3.3 24 173.4 246 256.9 381.6 121.3 6.9-7.8-12.6-28.4-20.7-20.4zM213.6 306.6c0 4 4.3 7.3 5.5 8.5 3 3 6.1 4.4 8.5 4.4 3.8 0 2.6.2 22.3-19.5 19.6 19.3 19.1 19.5 22.3 19.5 5.4 0 18.5-10.4 10.7-18.2L265.6 284l18.2-18.2c6.3-6.8-10.1-21.8-16.2-15.7L249.7 268c-18.6-18.8-18.4-19.5-21.5-19.5-5 0-18 11.7-12.4 17.3L234 284c-18.1 17.9-20.4 19.2-20.4 22.6z\"}}]})(props);\n};\nexport function FaAccessibleIcon (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 448 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M423.9 255.8L411 413.1c-3.3 40.7-63.9 35.1-60.6-4.9l10-122.5-41.1 2.3c10.1 20.7 15.8 43.9 15.8 68.5 0 41.2-16.1 78.7-42.3 106.5l-39.3-39.3c57.9-63.7 13.1-167.2-74-167.2-25.9 0-49.5 9.9-67.2 26L73 243.2c22-20.7 50.1-35.1 81.4-40.2l75.3-85.7-42.6-24.8-51.6 46c-30 26.8-70.6-18.5-40.5-45.4l68-60.7c9.8-8.8 24.1-10.2 35.5-3.6 0 0 139.3 80.9 139.5 81.1 16.2 10.1 20.7 36 6.1 52.6L285.7 229l106.1-5.9c18.5-1.1 33.6 14.4 32.1 32.7zm-64.9-154c28.1 0 50.9-22.8 50.9-50.9C409.9 22.8 387.1 0 359 0c-28.1 0-50.9 22.8-50.9 50.9 0 28.1 22.8 50.9 50.9 50.9zM179.6 456.5c-80.6 0-127.4-90.6-82.7-156.1l-39.7-39.7C36.4 287 24 320.3 24 356.4c0 130.7 150.7 201.4 251.4 122.5l-39.7-39.7c-16 10.9-35.3 17.3-56.1 17.3z\"}}]})(props);\n};\nexport function FaAccusoft (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 640 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M322.1 252v-1l-51.2-65.8s-12 1.6-25 15.1c-9 9.3-242.1 239.1-243.4 240.9-7 10 1.6 6.8 15.7 1.7.8 0 114.5-36.6 114.5-36.6.5-.6-.1-.1.6-.6-.4-5.1-.8-26.2-1-27.7-.6-5.2 2.2-6.9 7-8.9l92.6-33.8c.6-.8 88.5-81.7 90.2-83.3zm160.1 120.1c13.3 16.1 20.7 13.3 30.8 9.3 3.2-1.2 115.4-47.6 117.8-48.9 8-4.3-1.7-16.7-7.2-23.4-2.1-2.5-205.1-245.6-207.2-248.3-9.7-12.2-14.3-12.9-38.4-12.8-10.2 0-106.8.5-116.5.6-19.2.1-32.9-.3-19.2 16.9C250 75 476.5 365.2 482.2 372.1zm152.7 1.6c-2.3-.3-24.6-4.7-38-7.2 0 0-115 50.4-117.5 51.6-16 7.3-26.9-3.2-36.7-14.6l-57.1-74c-5.4-.9-60.4-9.6-65.3-9.3-3.1.2-9.6.8-14.4 2.9-4.9 2.1-145.2 52.8-150.2 54.7-5.1 2-11.4 3.6-11.1 7.6.2 2.5 2 2.6 4.6 3.5 2.7.8 300.9 67.6 308 69.1 15.6 3.3 38.5 10.5 53.6 1.7 2.1-1.2 123.8-76.4 125.8-77.8 5.4-4 4.3-6.8-1.7-8.2z\"}}]})(props);\n};\nexport function FaAdn (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 496 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M248 167.5l64.9 98.8H183.1l64.9-98.8zM496 256c0 136.9-111.1 248-248 248S0 392.9 0 256 111.1 8 248 8s248 111.1 248 248zm-99.8 82.7L248 115.5 99.8 338.7h30.4l33.6-51.7h168.6l33.6 51.7h30.2z\"}}]})(props);\n};\nexport function FaAdversal (props) {\n return GenIcon({\"tag\":\"svg\",\"attr\":{\"viewBox\":\"0 0 512 512\"},\"child\":[{\"tag\":\"path\",\"attr\":{\"d\":\"M482.1 32H28.7C5.8 32 0 37.9 0 60.9v390.2C0 474.4 5.8 480 28.7 480h453.4c24.4 0 29.9-5.2 29.9-29.7V62.2c0-24.6-5.4-30.2-29.9-30.2zM178.4 220.3c-27.5-20.2-72.1-8.7-84.2 23.4-4.3 11.1-9.3 9.5-17.5 8.3-9.7-1.5-17.2-3.2-22.5-5.5-28.8-11.4 8.6-55.3 24.9-64.3 41.1-21.4 83.4-22.2 125.3-4.8 40.9 16.8 34.5 59.2 34.5 128.5 2.7 25.8-4.3 58.3 9.3 88.8 1.9 4.4.4 7.9-2.7 10.7-8.4 6.7-39.3 2Line truncated
|
||||||
"mappings": "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 truncated
|
"mappings": "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 truncated
|
||||||
"names": []
|
"names": []
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
require_react_dom
|
require_react_dom
|
||||||
} from "./chunk-TDH2IRYZ.js";
|
} from "./chunk-PJEEZAML.js";
|
||||||
import {
|
import {
|
||||||
require_react
|
require_react
|
||||||
} from "./chunk-QRULMDK5.js";
|
} from "./chunk-DRWLMN53.js";
|
||||||
import {
|
import {
|
||||||
__toESM
|
__toESM
|
||||||
} from "./chunk-G3PMV62Z.js";
|
} from "./chunk-G3PMV62Z.js";
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
require_react
|
require_react
|
||||||
} from "./chunk-QRULMDK5.js";
|
} from "./chunk-DRWLMN53.js";
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
export default require_react();
|
export default require_react();
|
||||||
//# sourceMappingURL=react.js.map
|
//# sourceMappingURL=react.js.map
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
require_react
|
require_react
|
||||||
} from "./chunk-QRULMDK5.js";
|
} from "./chunk-DRWLMN53.js";
|
||||||
import {
|
import {
|
||||||
__commonJS
|
__commonJS
|
||||||
} from "./chunk-G3PMV62Z.js";
|
} from "./chunk-G3PMV62Z.js";
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../../react/cjs/react-jsx-dev-runtime.development.js", "../../react/jsx-dev-runtime.js"],
|
"sources": ["../../react/cjs/react-jsx-dev-runtime.development.js", "../../react/jsx-dev-runtime.js"],
|
||||||
"sourcesContent": ["/**\r\n * @license React\r\n * react-jsx-dev-runtime.development.js\r\n *\r\n * Copyright (c) Facebook, Inc. and its affiliates.\r\n *\r\n * This source code is licensed under the MIT license found in the\r\n * LICENSE file in the root directory of this source tree.\r\n */\r\n\r\n'use strict';\r\n\r\nif (process.env.NODE_ENV !== \"production\") {\r\n (function() {\r\n'use strict';\r\n\r\nvar React = require('react');\r\n\r\n// ATTENTION\r\n// When adding new symbols to this file,\r\n// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols'\r\n// The Symbol used to tag the ReactElement-like types.\r\nvar REACT_ELEMENT_TYPE = Symbol.for('react.element');\r\nvar REACT_PORTAL_TYPE = Symbol.for('react.portal');\r\nvar REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');\r\nvar REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode');\r\nvar REACT_PROFILER_TYPE = Symbol.for('react.profiler');\r\nvar REACT_PROVIDER_TYPE = Symbol.for('react.provider');\r\nvar REACT_CONTEXT_TYPE = Symbol.for('react.context');\r\nvar REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref');\r\nvar REACT_SUSPENSE_TYPE = Symbol.for('react.suspense');\r\nvar REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list');\r\nvar REACT_MEMO_TYPE = Symbol.for('react.memo');\r\nvar REACT_LAZY_TYPE = Symbol.for('react.lazy');\r\nvar REACT_OFFSCREEN_TYPE = Symbol.for('react.offscreen');\r\nvar MAYBE_ITERATOR_SYMBOL = Symbol.iterator;\r\nvar FAUX_ITERATOR_SYMBOL = '@@iterator';\r\nfunction getIteratorFn(maybeIterable) {\r\n if (maybeIterable === null || typeof maybeIterable !== 'object') {\r\n return null;\r\n }\r\n\r\n var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];\r\n\r\n if (typeof maybeIterator === 'function') {\r\n return maybeIterator;\r\n }\r\n\r\n return null;\r\n}\r\n\r\nvar ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\r\n\r\nfunction error(format) {\r\n {\r\n {\r\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\r\n args[_key2 - 1] = arguments[_key2];\r\n }\r\n\r\n printWarning('error', format, args);\r\n }\r\n }\r\n}\r\n\r\nfunction printWarning(level, format, args) {\r\n // When changing this logic, you might want to also\r\n // update consoleWithStackDev.www.js as well.\r\n {\r\n var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\r\n var stack = ReactDebugCurrentFrame.getStackAddendum();\r\n\r\n if (stack !== '') {\r\n format += '%s';\r\n args = args.concat([stack]);\r\n } // eslint-disable-next-line react-internal/safe-string-coercion\r\n\r\n\r\n var argsWithFormat = args.map(function (item) {\r\n return String(item);\r\n }); // Careful: RN currently depends on this prefix\r\n\r\n argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it\r\n // breaks IE9: https://github.com/facebook/react/issues/13610\r\n // eslint-disable-next-line react-internal/no-production-logging\r\n\r\n Function.prototype.apply.call(console[level], console, argsWithFormat);\r\n }\r\n}\r\n\r\n// -----------------------------------------------------------------------------\r\n\r\nvar enableScopeAPI = false; // Experimental Create Event Handle API.\r\nvar enableCacheElement = false;\r\nvar enableTransitionTracing = false; // No known bugs, but needs performance testing\r\n\r\nvar enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber\r\n// stuff. Intended to enable React core members to more easily debug scheduling\r\n// issues in DEV builds.\r\n\r\nvar enableDebugTracing = false; // Track which Fiber(s) schedule render work.\r\n\r\nvar REACT_MODULE_REFERENCE;\r\n\r\n{\r\n REACT_MODULE_REFERENCE = Symbol.for('react.module.reference');\r\n}\r\n\r\nfunction isValidElementType(type) {\r\n if (typeof type === 'string' || typeof type === 'function') {\r\n return true;\r\n } // Note: typeof might be other than 'symbol' or 'number' (e.g. if it's a polyfill).\r\n\r\n\r\n 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 ) {\r\n return true;\r\n }\r\n\r\n if (typeof type === 'object' && type !== null) {\r\n 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\r\n // types supported by any Flight configuration anywhere since\r\n // we don't know which Flight buLine truncated
|
"sourcesContent": ["/**\n * @license React\n * react-jsx-dev-runtime.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n'use strict';\n\nif (process.env.NODE_ENV !== \"production\") {\n (function() {\n'use strict';\n\nvar React = require('react');\n\n// ATTENTION\n// When adding new symbols to this file,\n// Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols'\n// The Symbol used to tag the ReactElement-like types.\nvar REACT_ELEMENT_TYPE = Symbol.for('react.element');\nvar REACT_PORTAL_TYPE = Symbol.for('react.portal');\nvar REACT_FRAGMENT_TYPE = Symbol.for('react.fragment');\nvar REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode');\nvar REACT_PROFILER_TYPE = Symbol.for('react.profiler');\nvar REACT_PROVIDER_TYPE = Symbol.for('react.provider');\nvar REACT_CONTEXT_TYPE = Symbol.for('react.context');\nvar REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref');\nvar REACT_SUSPENSE_TYPE = Symbol.for('react.suspense');\nvar REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list');\nvar REACT_MEMO_TYPE = Symbol.for('react.memo');\nvar REACT_LAZY_TYPE = Symbol.for('react.lazy');\nvar REACT_OFFSCREEN_TYPE = Symbol.for('react.offscreen');\nvar MAYBE_ITERATOR_SYMBOL = Symbol.iterator;\nvar FAUX_ITERATOR_SYMBOL = '@@iterator';\nfunction getIteratorFn(maybeIterable) {\n if (maybeIterable === null || typeof maybeIterable !== 'object') {\n return null;\n }\n\n var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];\n\n if (typeof maybeIterator === 'function') {\n return maybeIterator;\n }\n\n return null;\n}\n\nvar ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nfunction error(format) {\n {\n {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n\n printWarning('error', format, args);\n }\n }\n}\n\nfunction printWarning(level, format, args) {\n // When changing this logic, you might want to also\n // update consoleWithStackDev.www.js as well.\n {\n var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n var stack = ReactDebugCurrentFrame.getStackAddendum();\n\n if (stack !== '') {\n format += '%s';\n args = args.concat([stack]);\n } // eslint-disable-next-line react-internal/safe-string-coercion\n\n\n var argsWithFormat = args.map(function (item) {\n return String(item);\n }); // Careful: RN currently depends on this prefix\n\n argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it\n // breaks IE9: https://github.com/facebook/react/issues/13610\n // eslint-disable-next-line react-internal/no-production-logging\n\n Function.prototype.apply.call(console[level], console, argsWithFormat);\n }\n}\n\n// -----------------------------------------------------------------------------\n\nvar enableScopeAPI = false; // Experimental Create Event Handle API.\nvar enableCacheElement = false;\nvar enableTransitionTracing = false; // No known bugs, but needs performance testing\n\nvar enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber\n// stuff. Intended to enable React core members to more easily debug scheduling\n// issues in DEV builds.\n\nvar enableDebugTracing = false; // Track which Fiber(s) schedule render work.\n\nvar REACT_MODULE_REFERENCE;\n\n{\n REACT_MODULE_REFERENCE = Symbol.for('react.module.reference');\n}\n\nfunction isValidElementType(type) {\n if (typeof type === 'string' || typeof type === 'function') {\n return true;\n } // Note: typeof might be other than 'symbol' or 'number' (e.g. if it's a polyfill).\n\n\n 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 ) {\n return true;\n }\n\n if (typeof type === 'object' && type !== null) {\n 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\n // types supported by any Flight configuration anywhere since\n // we don't know which Flight build this will end up being used\n // with.\n type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== undefined) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction getWrappedName(outerType, innerType, wrapperNamLine truncated
|
||||||
"mappings": "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 truncated
|
"mappings": "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 truncated
|
||||||
"names": ["ReactDebugCurrentFrame"]
|
"names": ["ReactDebugCurrentFrame"]
|
||||||
}
|
}
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
require_jsx_runtime
|
require_jsx_runtime
|
||||||
} from "./chunk-NMLHVZ76.js";
|
} from "./chunk-6PXSGDAH.js";
|
||||||
import "./chunk-QRULMDK5.js";
|
import "./chunk-DRWLMN53.js";
|
||||||
import "./chunk-G3PMV62Z.js";
|
import "./chunk-G3PMV62Z.js";
|
||||||
export default require_jsx_runtime();
|
export default require_jsx_runtime();
|
||||||
//# sourceMappingURL=react_jsx-runtime.js.map
|
//# sourceMappingURL=react_jsx-runtime.js.map
|
||||||
Generated
+1166
-125
File diff suppressed because it is too large.
Load diff
+2
-8
@@ -5,20 +5,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tabler/icons-react": "^3.36.0",
|
"@tabler/icons-react": "^3.36.0",
|
||||||
"appwrite": "^13.0.0",
|
"appwrite": "^13.0.0",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"motion": "^12.23.26",
|
|
||||||
"postprocessing": "^6.38.2",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"react-router-dom": "^6.20.0",
|
"react-markdown": "^10.1.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"react-router-dom": "^6.20.0"
|
||||||
"three": "^0.182.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.0",
|
|
||||||
"@types/react-dom": "^18.2.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+31
-79
@@ -1,126 +1,78 @@
|
|||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import { AuthProvider, useAuth } from './context/AuthContext'
|
import { AuthProvider, useAuth } from './context/AuthContext'
|
||||||
import Navbar from './components/Navbar'
|
import Sidebar from './components/Navbar'
|
||||||
import Footer from './components/Footer'
|
import Footer from './components/Footer'
|
||||||
import PixelBlast from './components/PixelBlast'
|
|
||||||
import LoginPage from './pages/LoginPage'
|
import LoginPage from './pages/LoginPage'
|
||||||
import TicketsPage from './pages/TicketsPage'
|
import TicketsPage from './pages/TicketsPage'
|
||||||
import DashboardPage from './pages/DashboardPage'
|
|
||||||
import AssetsPage from './pages/AssetsPage'
|
|
||||||
import PlanboardPage from './pages/PlanboardPage'
|
import PlanboardPage from './pages/PlanboardPage'
|
||||||
import ProjectsPage from './pages/ProjectsPage'
|
import ProjectsPage from './pages/ProjectsPage'
|
||||||
import ReportsPage from './pages/ReportsPage'
|
import RoadmapsPage from './pages/RoadmapsPage'
|
||||||
import DocsPage from './pages/DocsPage'
|
import RoadmapDetailPage from './pages/RoadmapDetailPage'
|
||||||
import AdminPage from './pages/AdminPage'
|
import AdminPage from './pages/AdminPage'
|
||||||
|
import CustomersPage from './pages/CustomersPage'
|
||||||
|
import LeadsPage from './pages/LeadsPage'
|
||||||
|
import CustomerDetailPage from './pages/CustomerDetailPage'
|
||||||
|
import FinancePage from './pages/FinancePage'
|
||||||
|
|
||||||
function ProtectedRoute({ children }) {
|
function ProtectedRoute({ children }) {
|
||||||
const { user, loading } = useAuth()
|
const { user, loading } = useAuth()
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center p-4">
|
<div className="text-center p-4" style={{ marginTop: 80 }}>
|
||||||
<div className="spinner"></div>
|
<div className="spinner"></div>
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <Navigate to="/login" replace />
|
return <Navigate to="/login" replace />
|
||||||
}
|
}
|
||||||
|
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={user ? <Navigate to="/tickets" replace /> : <LoginPage />} />
|
<Route path="/login" element={user ? <Navigate to="/tickets" replace /> : <LoginPage />} />
|
||||||
<Route path="/" element={<Navigate to="/tickets" replace />} />
|
<Route path="/" element={<Navigate to="/tickets" replace />} />
|
||||||
<Route path="/tickets" element={<ProtectedRoute><TicketsPage /></ProtectedRoute>} />
|
<Route path="/tickets" element={<ProtectedRoute><TicketsPage /></ProtectedRoute>} />
|
||||||
<Route path="/dashboard" element={<ProtectedRoute><DashboardPage /></ProtectedRoute>} />
|
<Route path="/customers" element={<ProtectedRoute><CustomersPage /></ProtectedRoute>} />
|
||||||
<Route path="/assets" element={<ProtectedRoute><AssetsPage /></ProtectedRoute>} />
|
<Route path="/customers/:id" element={<ProtectedRoute><CustomerDetailPage /></ProtectedRoute>} />
|
||||||
|
<Route path="/leads" element={<ProtectedRoute><LeadsPage /></ProtectedRoute>} />
|
||||||
|
<Route path="/finance" element={<ProtectedRoute><FinancePage /></ProtectedRoute>} />
|
||||||
<Route path="/planboard" element={<ProtectedRoute><PlanboardPage /></ProtectedRoute>} />
|
<Route path="/planboard" element={<ProtectedRoute><PlanboardPage /></ProtectedRoute>} />
|
||||||
<Route path="/projects" element={<ProtectedRoute><ProjectsPage /></ProtectedRoute>} />
|
<Route path="/projects" element={<ProtectedRoute><ProjectsPage /></ProtectedRoute>} />
|
||||||
<Route path="/reports" element={<ProtectedRoute><ReportsPage /></ProtectedRoute>} />
|
<Route path="/roadmaps" element={<ProtectedRoute><RoadmapsPage /></ProtectedRoute>} />
|
||||||
<Route path="/docs" element={<ProtectedRoute><DocsPage /></ProtectedRoute>} />
|
<Route path="/roadmaps/:id" element={<ProtectedRoute><RoadmapDetailPage /></ProtectedRoute>} />
|
||||||
<Route path="/admin" element={<ProtectedRoute><AdminPage /></ProtectedRoute>} />
|
<Route path="/admin" element={<ProtectedRoute><AdminPage /></ProtectedRoute>} />
|
||||||
|
{/* Alte Links (z. B. die entfernten Seiten Assets, Reports, Docs) landen bei den Tickets */}
|
||||||
|
<Route path="*" element={<Navigate to="/tickets" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return <AppRoutes />
|
||||||
<>
|
}
|
||||||
{/* PixelBlast Background für Login-Seite */}
|
|
||||||
<div style={{
|
return (
|
||||||
position: 'fixed',
|
<div className="app-shell">
|
||||||
top: 0,
|
<Sidebar />
|
||||||
left: 0,
|
<div className="app-main">
|
||||||
width: '100vw',
|
<div className="app-scroll">
|
||||||
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 />
|
<AppRoutes />
|
||||||
</div>
|
</div>
|
||||||
</>
|
<Footer />
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* 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>
|
||||||
|
</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>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
Lato Regular 1.105 (lato-regular.ttf)
|
||||||
|
Copyright (c) 2010-2013 by tyPoland Lukasz Dziedzic with Reserved Font Name "Lato".
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
@@ -0,0 +1,97 @@
|
|||||||
|
import { FaTrash, FaCodeBranch } from 'react-icons/fa'
|
||||||
|
import { FaGlobe, FaCodeCommit, FaClock } from 'react-icons/fa6'
|
||||||
|
import ProjectReadme from './ProjectReadme'
|
||||||
|
import GiteaLinkButton from './GiteaLinkButton'
|
||||||
|
import PreviewLinkButton from './PreviewLinkButton'
|
||||||
|
|
||||||
|
const STATUS_COLORS = {
|
||||||
|
ready: '#10b981',
|
||||||
|
live: '#10b981',
|
||||||
|
online: '#10b981',
|
||||||
|
provisioning: '#f59e0b',
|
||||||
|
pending: '#f59e0b',
|
||||||
|
building: '#3b82f6',
|
||||||
|
error: '#ef4444',
|
||||||
|
failed: '#ef4444',
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ label, value }) {
|
||||||
|
if (!value) return null
|
||||||
|
const color = STATUS_COLORS[String(value).toLowerCase()] || '#a0aec0'
|
||||||
|
return (
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#cbd5e0', marginRight: 12 }}>
|
||||||
|
{label}:
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color }}>
|
||||||
|
<span style={{ width: 8, height: 8, borderRadius: '50%', background: color, display: 'inline-block' }} />
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AssignedProjectCard({ project, onUnassign, showActions = false }) {
|
||||||
|
const lastPush = project.lastPushAt || project.giteaSyncedAt
|
||||||
|
return (
|
||||||
|
<div style={{ background: 'rgba(30,41,59,0.5)', border: '1px solid rgba(59,130,246,0.25)', borderRadius: 10, padding: '12px 14px', marginBottom: 12 }}>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<strong>{project.projectName}</strong>
|
||||||
|
{project.subdomain && <span className="text-grey" style={{ marginLeft: 8, fontSize: 13 }}>({project.subdomain})</span>}
|
||||||
|
{project.repoFullName && (
|
||||||
|
<div className="text-grey" style={{ fontSize: 12, marginTop: 4 }}>
|
||||||
|
<FaCodeBranch style={{ marginRight: 4 }} />{project.repoFullName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{project.liveDomain && (
|
||||||
|
<div style={{ fontSize: 12, marginTop: 4 }}>
|
||||||
|
<FaGlobe style={{ marginRight: 4, color: '#34d399' }} />
|
||||||
|
<a href={`https://${project.liveDomain}`} target="_blank" rel="noopener noreferrer" style={{ color: '#34d399', textDecoration: 'none' }}>
|
||||||
|
{project.liveDomain}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexShrink: 0 }}>
|
||||||
|
<PreviewLinkButton href={project.previewUrl} />
|
||||||
|
<GiteaLinkButton href={project.giteaRepoUrl} />
|
||||||
|
{showActions && onUnassign && (
|
||||||
|
<button type="button" className="btn btn-sm" onClick={() => onUnassign(project.$id)}><FaTrash /></button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status- und Provisioning-Zeile */}
|
||||||
|
<div style={{ marginTop: 10, display: 'flex', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
<StatusBadge label="Status" value={project.status} />
|
||||||
|
<StatusBadge label="Provisioning" value={project.provisioningStatus} />
|
||||||
|
{project.hasPreview && <StatusBadge label="Preview" value="aktiv" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Letzter Commit */}
|
||||||
|
{(project.lastCommitMessage || project.lastCommitSha) && (
|
||||||
|
<div style={{ marginTop: 8, fontSize: 12, color: '#94a3b8', background: 'rgba(0,0,0,0.2)', borderRadius: 8, padding: '8px 10px' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
|
<FaCodeCommit style={{ color: '#3b82f6' }} />
|
||||||
|
{project.lastCommitSha && <code style={{ color: '#cbd5e0' }}>{String(project.lastCommitSha).slice(0, 7)}</code>}
|
||||||
|
{project.lastCommitBranch || project.lastPushBranch ? (
|
||||||
|
<span className="text-grey">({project.lastPushBranch || project.lastCommitBranch})</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{project.lastCommitMessage && (
|
||||||
|
<div style={{ marginTop: 4, color: '#cbd5e0', whiteSpace: 'pre-wrap' }}>{project.lastCommitMessage}</div>
|
||||||
|
)}
|
||||||
|
<div style={{ marginTop: 4, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||||
|
{project.lastCommitAuthor && <span>von {project.lastCommitAuthor}</span>}
|
||||||
|
{lastPush && (
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<FaClock /> {new Date(lastPush).toLocaleString('de-DE')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{project.repoFullName && <ProjectReadme repoFullName={project.repoFullName} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { FaCheck, FaCopy, FaEye, FaEyeSlash } from 'react-icons/fa6'
|
||||||
|
|
||||||
|
async function copyText(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
} catch {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
textarea.setAttribute('readonly', '')
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.left = '-9999px'
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CopyableCredential({ value, secret = false, label = 'Wert' }) {
|
||||||
|
const [visible, setVisible] = useState(!secret)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const timeoutRef = useRef(null)
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setVisible(!secret)
|
||||||
|
}, [secret, text])
|
||||||
|
|
||||||
|
useEffect(() => () => clearTimeout(timeoutRef.current), [])
|
||||||
|
|
||||||
|
if (!text) return <span className="credential-empty">–</span>
|
||||||
|
|
||||||
|
const handleCopy = async (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
await copyText(text)
|
||||||
|
setCopied(true)
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = setTimeout(() => setCopied(false), 1800)
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayValue = secret && !visible ? '••••••••••••' : text
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="credential">
|
||||||
|
<span
|
||||||
|
className={`credential-value ${secret && !visible ? 'is-masked' : ''}`}
|
||||||
|
title={secret && !visible ? `${label} anzeigen` : text}
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</span>
|
||||||
|
{secret && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="credential-action"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
setVisible((current) => !current)
|
||||||
|
}}
|
||||||
|
title={visible ? `${label} ausblenden` : `${label} anzeigen`}
|
||||||
|
aria-label={visible ? `${label} ausblenden` : `${label} anzeigen`}
|
||||||
|
>
|
||||||
|
{visible ? <FaEyeSlash /> : <FaEye />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="credential-action"
|
||||||
|
onClick={handleCopy}
|
||||||
|
title={copied ? 'Kopiert' : `${label} kopieren`}
|
||||||
|
aria-label={copied ? `${label} kopiert` : `${label} kopieren`}
|
||||||
|
>
|
||||||
|
{copied ? <FaCheck className="credential-check" /> : <FaCopy />}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCustomerPortalPassword(customer) {
|
||||||
|
return customer?.portalPassword || customer?.password || ''
|
||||||
|
}
|
||||||
@@ -1,90 +1,75 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { FaTimes } from 'react-icons/fa'
|
||||||
import { useAdminConfig } from '../hooks/useAdminConfig'
|
|
||||||
import { useEmployees } from '../hooks/useEmployees'
|
import { useEmployees } from '../hooks/useEmployees'
|
||||||
|
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||||
|
import {
|
||||||
|
SERVICE_TYPES,
|
||||||
|
todayIso,
|
||||||
|
isoToGermanDate,
|
||||||
|
timeToWoms,
|
||||||
|
buildRequesterSuggestions,
|
||||||
|
rankSuggestions,
|
||||||
|
} from '../lib/ticketForm'
|
||||||
|
import { priorityFor } from '../lib/ticketStatus'
|
||||||
|
import { AutocompleteInput, DateTimeInput } from './ui'
|
||||||
|
|
||||||
// Fallback-Werte falls Config nicht geladen werden kann
|
// Neue Tickets gibt es aktuell nur fuer diese Arten; alte Tickets behalten ihre Art.
|
||||||
const DEFAULT_TICKET_TYPES = [
|
// Migration = ein Kunde, den wir von einem anderen Anbieter uebernehmen.
|
||||||
'Home Office', 'Holidays', 'Trip', 'Supportrequest', 'Change Request',
|
const TICKET_TYPES = [
|
||||||
'Maintenance', 'Project', 'Controlling', 'Development', 'Documentation',
|
{ value: 'Webpage', label: 'Webpage' },
|
||||||
'Meeting/Conference', 'IT Management', 'IT Security', 'Procurement',
|
{ value: 'Migration', label: 'Migration (Übernahme von anderem Anbieter)' },
|
||||||
'Rollout', 'Emergency Call', 'Other Services'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const DEFAULT_SYSTEMS = [
|
// Datum als "yyyy-mm-dd", Uhrzeit als "hh:mm" (Werte der Kalender-Felder).
|
||||||
'Account View', 'Client', 'Cofano', 'Credentials', 'Diamant', 'Docuware',
|
// Die Service-Art fragt das Formular nicht mehr ab: gespeichert wird Fernwartung.
|
||||||
'EDI', 'eMail', 'Employee', 'Invoice', 'LBase', 'Medical Office', 'Network',
|
const emptyForm = () => ({
|
||||||
'O365', 'PDF Viewer', 'Printer', 'Reports', 'Server', 'Time Tracking',
|
customerId: '',
|
||||||
'TK', 'TOS', 'Vivendi NG', 'VGM', '(W)LAN', '(W)WAN', 'WOMS', 'n/a'
|
type: TICKET_TYPES[0].value,
|
||||||
]
|
serviceType: SERVICE_TYPES[0].value,
|
||||||
|
urgent: false,
|
||||||
const DEFAULT_RESPONSE_LEVELS = [
|
topic: '',
|
||||||
'USER', 'KEY USER', 'Helpdesk', 'Support', 'Admin', 'FS/FE', '24/7',
|
requestedBy: '',
|
||||||
'TECH MGMT', 'Backoffice', 'BUSI MGMT', 'n/a'
|
startDate: todayIso(),
|
||||||
]
|
startTime: '',
|
||||||
|
deadline: todayIso(),
|
||||||
const DEFAULT_SERVICE_TYPES = ['Remote', 'On Site', 'Off Site']
|
endTime: '',
|
||||||
|
details: ''
|
||||||
const DEFAULT_PRIORITIES = [
|
})
|
||||||
{ value: 0, label: 'None' },
|
|
||||||
{ value: 1, label: 'Low' },
|
|
||||||
{ value: 2, label: 'Medium' },
|
|
||||||
{ value: 3, label: 'High' },
|
|
||||||
{ value: 4, label: 'Critical' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const today = new Date().toLocaleDateString('de-DE')
|
|
||||||
|
|
||||||
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
|
export default function CreateTicketModal({ isOpen, onClose, onCreate, customers = [] }) {
|
||||||
const { config } = useAdminConfig()
|
|
||||||
const { employees } = useEmployees()
|
const { employees } = useEmployees()
|
||||||
|
|
||||||
// Verwende Config-Werte oder Fallbacks
|
const [formData, setFormData] = useState(emptyForm)
|
||||||
const TICKET_TYPES = config?.ticketTypes || DEFAULT_TICKET_TYPES
|
const [recentTickets, setRecentTickets] = useState([])
|
||||||
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: '',
|
|
||||||
systemType: '',
|
|
||||||
responseLevel: '',
|
|
||||||
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,
|
|
||||||
endTime: '',
|
|
||||||
estimate: '30',
|
|
||||||
mailCopyTo: '',
|
|
||||||
sendNotification: false,
|
|
||||||
details: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
// Setze Default-Werte wenn Config geladen ist oder Modal geöffnet wird
|
// Reset error when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && (TICKET_TYPES.length > 0 || SERVICE_TYPES.length > 0)) {
|
if (isOpen) setError('')
|
||||||
setFormData(prev => ({
|
}, [isOpen])
|
||||||
...prev,
|
|
||||||
type: prev.type || TICKET_TYPES[0] || 'Supportrequest',
|
// Namen aus den letzten Tickets fuer die Autovervollstaendigung
|
||||||
serviceType: prev.serviceType || SERVICE_TYPES[0] || 'Remote',
|
useEffect(() => {
|
||||||
priority: prev.priority || (PRIORITIES.find(p => p.value === 1)?.value || 1)
|
if (!isOpen) return
|
||||||
}))
|
let cancelled = false
|
||||||
|
databases
|
||||||
|
.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [Query.orderDesc('$createdAt'), Query.limit(200)])
|
||||||
|
.then((response) => {
|
||||||
|
if (!cancelled) setRecentTickets(response.documents || [])
|
||||||
|
})
|
||||||
|
.catch((err) => console.warn('Vorschläge aus früheren Tickets nicht geladen:', err))
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
}
|
}
|
||||||
// Reset error when modal opens
|
}, [isOpen])
|
||||||
if (isOpen) {
|
|
||||||
setError('')
|
const suggestions = useMemo(
|
||||||
}
|
() => buildRequesterSuggestions({ customers, employees, tickets: recentTickets }),
|
||||||
}, [isOpen, TICKET_TYPES, SERVICE_TYPES, PRIORITIES])
|
[customers, employees, recentTickets]
|
||||||
|
)
|
||||||
|
const getSuggestions = (text) => rankSuggestions(suggestions, text, { customerId: formData.customerId })
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const handleChange = (field, value) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }))
|
setFormData(prev => ({ ...prev, [field]: value }))
|
||||||
@@ -92,34 +77,37 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
|||||||
if (error) setError('')
|
if (error) setError('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Das Ende darf nicht vor dem Start liegen, deshalb rueckt es mit
|
||||||
|
const handleStartDateChange = (value) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
startDate: value,
|
||||||
|
deadline: value && prev.deadline && prev.deadline < value ? value : prev.deadline
|
||||||
|
}))
|
||||||
|
if (error) setError('')
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
|
const customer = customers.find(c => c.$id === formData.customerId)
|
||||||
|
const { urgent, ...fields } = formData
|
||||||
try {
|
try {
|
||||||
const result = await onCreate(formData)
|
const result = await onCreate({
|
||||||
|
...fields,
|
||||||
|
priority: priorityFor(urgent),
|
||||||
|
customerName: customer?.name || '',
|
||||||
|
customerLocation: customer?.location || '',
|
||||||
|
startDate: isoToGermanDate(formData.startDate),
|
||||||
|
startTime: timeToWoms(formData.startTime),
|
||||||
|
deadline: isoToGermanDate(formData.deadline),
|
||||||
|
endTime: timeToWoms(formData.endTime)
|
||||||
|
})
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onClose()
|
onClose()
|
||||||
setFormData({
|
setFormData(emptyForm())
|
||||||
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 {
|
} else {
|
||||||
setError(result.error || 'Fehler beim Erstellen des Tickets')
|
setError(result.error || 'Fehler beim Erstellen des Tickets')
|
||||||
}
|
}
|
||||||
@@ -139,26 +127,26 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
|||||||
<FaTimes />
|
<FaTimes />
|
||||||
</span>
|
</span>
|
||||||
<div className="overlay-content">
|
<div className="overlay-content">
|
||||||
<h2 className="mb-2">Create New Ticket</h2>
|
<h2 className="mb-2">Neues Ticket</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col col-6">
|
<div className="col col-6">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Customer ID</label>
|
<label className="form-label">Kunde</label>
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
value={formData.customerId}
|
value={formData.customerId}
|
||||||
onChange={(e) => handleChange('customerId', e.target.value)}
|
onChange={(e) => handleChange('customerId', e.target.value)}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Affected Customer</option>
|
<option value="">Kunde wählen</option>
|
||||||
{customers.map(c => (
|
{customers.map(c => (
|
||||||
<option key={c.$id} value={c.$id}>({c.code || ''}) {c.name || 'Unnamed'}</option>
|
<option key={c.$id} value={c.$id}>({c.code || ''}) {c.name || 'Unnamed'}</option>
|
||||||
))}
|
))}
|
||||||
@@ -166,109 +154,46 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Work Order Type</label>
|
<label className="form-label">Art</label>
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
value={formData.type}
|
value={formData.type}
|
||||||
onChange={(e) => handleChange('type', e.target.value)}
|
onChange={(e) => handleChange('type', e.target.value)}
|
||||||
>
|
>
|
||||||
{TICKET_TYPES.map(type => (
|
{TICKET_TYPES.map(type => (
|
||||||
<option key={type} value={type}>{type}</option>
|
<option key={type.value} value={type.value}>{type.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Affected System</label>
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||||
<select
|
<input
|
||||||
className="form-control"
|
type="checkbox"
|
||||||
value={formData.systemType}
|
checked={formData.urgent}
|
||||||
onChange={(e) => handleChange('systemType', e.target.value)}
|
onChange={(e) => handleChange('urgent', e.target.checked)}
|
||||||
required
|
/>
|
||||||
>
|
Dringend
|
||||||
<option value="">Affected System</option>
|
</label>
|
||||||
{SYSTEMS.map(sys => (
|
|
||||||
<option key={sys} value={sys}>{sys}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group automatic-assignment-note">
|
||||||
<label className="form-label">Response Level</label>
|
<label className="form-label">Zuweisung</label>
|
||||||
<select
|
<p>
|
||||||
className="form-control"
|
{employees.length > 0
|
||||||
value={formData.responseLevel}
|
? `Automatisch an den Mitarbeiter mit der geringsten Anzahl offener Tickets (${employees.length} verfügbar).`
|
||||||
onChange={(e) => handleChange('responseLevel', e.target.value)}
|
: 'Es ist noch kein Mitarbeiter verfügbar. Das Ticket bleibt zunächst offen.'}
|
||||||
>
|
</p>
|
||||||
<option value="">Response Level</option>
|
|
||||||
{RESPONSE_LEVELS.map(level => (
|
|
||||||
<option key={level} value={level}>{level}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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)}
|
|
||||||
>
|
|
||||||
{SERVICE_TYPES.map(type => (
|
|
||||||
<option key={type} value={type}>{type}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Priority</label>
|
|
||||||
<select
|
|
||||||
className="form-control"
|
|
||||||
value={formData.priority}
|
|
||||||
onChange={(e) => handleChange('priority', parseInt(e.target.value))}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Priority Level</option>
|
|
||||||
{PRIORITIES.map(p => (
|
|
||||||
<option key={p.value} value={p.value}>{p.label}</option>
|
|
||||||
))}
|
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<div className="col col-6">
|
<div className="col col-6">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Topic</label>
|
<label className="form-label">Betreff</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
placeholder="Topic"
|
placeholder="Worum geht es?"
|
||||||
value={formData.topic}
|
value={formData.topic}
|
||||||
onChange={(e) => handleChange('topic', e.target.value)}
|
onChange={(e) => handleChange('topic', e.target.value)}
|
||||||
required
|
required
|
||||||
@@ -276,125 +201,62 @@ export default function CreateTicketModal({ isOpen, onClose, onCreate, customers
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Requested by</label>
|
<label className="form-label" htmlFor="ticket-requested-by">Angefragt von</label>
|
||||||
<input
|
<AutocompleteInput
|
||||||
type="text"
|
id="ticket-requested-by"
|
||||||
className="form-control"
|
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
value={formData.requestedBy}
|
value={formData.requestedBy}
|
||||||
onChange={(e) => handleChange('requestedBy', e.target.value)}
|
onChange={(value) => handleChange('requestedBy', value)}
|
||||||
|
getSuggestions={getSuggestions}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Requested for</label>
|
<label className="form-label" htmlFor="ticket-start-date">Beginn</label>
|
||||||
<input
|
<DateTimeInput
|
||||||
type="text"
|
id="ticket-start-date"
|
||||||
className="form-control"
|
timeLabel="Uhrzeit Beginn"
|
||||||
placeholder="Name"
|
date={formData.startDate}
|
||||||
value={formData.requestedFor}
|
time={formData.startTime}
|
||||||
onChange={(e) => handleChange('requestedFor', e.target.value)}
|
onDateChange={handleStartDateChange}
|
||||||
|
onTimeChange={(value) => handleChange('startTime', value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Start Date & Time</label>
|
<label className="form-label" htmlFor="ticket-end-date">Deadline</label>
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<DateTimeInput
|
||||||
<input
|
id="ticket-end-date"
|
||||||
type="text"
|
timeLabel="Uhrzeit Deadline"
|
||||||
className="form-control"
|
min={formData.startDate}
|
||||||
placeholder="dd.mm.yyyy"
|
date={formData.deadline}
|
||||||
value={formData.startDate}
|
time={formData.endTime}
|
||||||
onChange={(e) => handleChange('startDate', e.target.value)}
|
onDateChange={(value) => handleChange('deadline', value)}
|
||||||
style={{ flex: 1 }}
|
onTimeChange={(value) => handleChange('endTime', value)}
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="hhmm"
|
|
||||||
value={formData.startTime}
|
|
||||||
onChange={(e) => handleChange('startTime', e.target.value)}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">End Date & Time (Deadline)</label>
|
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="dd.mm.yyyy"
|
|
||||||
value={formData.deadline}
|
|
||||||
onChange={(e) => handleChange('deadline', e.target.value)}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="hhmm"
|
|
||||||
value={formData.endTime}
|
|
||||||
onChange={(e) => handleChange('endTime', e.target.value)}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Workload Estimate</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="xx minutes"
|
|
||||||
value={formData.estimate}
|
|
||||||
onChange={(e) => handleChange('estimate', e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label">Notification Recipient(s)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="name@domain.xx"
|
|
||||||
value={formData.mailCopyTo}
|
|
||||||
onChange={(e) => handleChange('mailCopyTo', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={formData.sendNotification}
|
|
||||||
onChange={(e) => handleChange('sendNotification', e.target.checked)}
|
|
||||||
/>
|
|
||||||
Send Notification?
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Details</label>
|
<label className="form-label">Beschreibung</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control"
|
className="form-control"
|
||||||
rows={5}
|
rows={5}
|
||||||
placeholder="Details"
|
placeholder="Was soll gemacht werden?"
|
||||||
value={formData.details}
|
value={formData.details}
|
||||||
onChange={(e) => handleChange('details', e.target.value)}
|
onChange={(e) => handleChange('details', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-dark"
|
className="btn btn-dark"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? 'Creating...' : 'CREATE NOW'}
|
{loading ? 'Wird angelegt …' : 'Ticket anlegen'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,55 +1,48 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { FaTimes } from 'react-icons/fa'
|
import { FaTimes } from 'react-icons/fa'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import { SERVICE_TYPES, todayIso, isoToGermanDate, timeToWoms, minutesBetween } from '../lib/ticketForm'
|
||||||
|
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
||||||
|
import { DateTimeInput } from './ui'
|
||||||
|
|
||||||
const SERVICE_TYPES = ['Remote', 'On Site', 'Off Site', 'COMMENT']
|
// Akquise-Tickets: Ergebnis des Kundenkontakts (statt technischer Status)
|
||||||
|
const ACQUISITION_STATUS = ['In Kontakt', 'Zugesagt', 'Abgesagt']
|
||||||
|
|
||||||
const STATUS_OPTIONS = [
|
// Die sechs Status; ein alter Status (z. B. "Assigned") bleibt als aktuelle Auswahl sichtbar
|
||||||
'Open',
|
function statusOptionsFor(current) {
|
||||||
'Closed',
|
if (!current || TICKET_STATUSES.some((s) => s.value === current)) return TICKET_STATUSES
|
||||||
'Awaiting',
|
return [{ value: current, label: `${statusLabel(current)} (bisher)` }, ...TICKET_STATUSES]
|
||||||
'Added Info',
|
}
|
||||||
'Occupied',
|
|
||||||
'Halted',
|
|
||||||
'Cancelled',
|
|
||||||
'Aborted',
|
|
||||||
'Assigned',
|
|
||||||
'In Test'
|
|
||||||
]
|
|
||||||
|
|
||||||
const RESPONSE_LEVELS = [
|
// Datum als "yyyy-mm-dd", Uhrzeit als "hh:mm" (Werte der Kalender-Felder).
|
||||||
'KEY USER',
|
// Die Service-Art kommt vom Ticket; das alte "Off Site" zaehlt als Fernwartung.
|
||||||
'1st Level',
|
const initialForm = (workorder, newStatus) => ({
|
||||||
'2nd Level',
|
serviceType: SERVICE_TYPES.some(t => t.value === workorder?.serviceType)
|
||||||
'3rd Level',
|
? workorder.serviceType
|
||||||
'FS/FE',
|
: SERVICE_TYPES[0].value,
|
||||||
'24/7',
|
newStatus,
|
||||||
'TECH MGMT',
|
totalTime: 0,
|
||||||
'Backoffice',
|
startDate: todayIso(),
|
||||||
'BUSI MGMT',
|
startTime: '',
|
||||||
'n/a'
|
endDate: todayIso(),
|
||||||
]
|
endTime: '',
|
||||||
|
details: '',
|
||||||
|
isComment: false,
|
||||||
|
countsToPlan: false
|
||||||
|
})
|
||||||
|
|
||||||
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate }) {
|
export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCreate }) {
|
||||||
const { user } = useAuth()
|
const { user } = useAuth()
|
||||||
const today = new Date().toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric'
|
|
||||||
})
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const isAcquisition = workorder?.type === 'Akquise'
|
||||||
serviceType: 'Remote',
|
const statusOptions = isAcquisition
|
||||||
newStatus: workorder?.status || 'Open',
|
? ACQUISITION_STATUS.map((s) => ({ value: s, label: s }))
|
||||||
newResponseLevel: workorder?.responseLevel || '',
|
: statusOptionsFor(workorder?.status)
|
||||||
totalTime: 0,
|
const defaultStatus = isAcquisition
|
||||||
startDate: today,
|
? (ACQUISITION_STATUS.includes(workorder?.status) ? workorder.status : 'In Kontakt')
|
||||||
startTime: '',
|
: (workorder?.status || 'Open')
|
||||||
endDate: today,
|
|
||||||
endTime: '',
|
const [formData, setFormData] = useState(() => initialForm(workorder, defaultStatus))
|
||||||
details: '',
|
|
||||||
isComment: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -58,63 +51,44 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
// Reset form wenn Modal geöffnet wird
|
// Reset form wenn Modal geöffnet wird
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && workorder) {
|
if (isOpen && workorder) {
|
||||||
setFormData({
|
setFormData(initialForm(workorder, defaultStatus))
|
||||||
serviceType: 'Remote',
|
|
||||||
newStatus: workorder.status || 'Open',
|
|
||||||
newResponseLevel: workorder.responseLevel || '',
|
|
||||||
totalTime: 0,
|
|
||||||
startDate: today,
|
|
||||||
startTime: '',
|
|
||||||
endDate: today,
|
|
||||||
endTime: '',
|
|
||||||
details: '',
|
|
||||||
isComment: false
|
|
||||||
})
|
|
||||||
setError('')
|
setError('')
|
||||||
setAutoCalculate(true)
|
setAutoCalculate(true)
|
||||||
}
|
}
|
||||||
}, [isOpen, workorder, today])
|
}, [isOpen, workorder, defaultStatus])
|
||||||
|
|
||||||
// Automatische Zeitberechnung
|
// Automatische Zeitberechnung
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoCalculate && formData.startTime && formData.endTime && !formData.isComment) {
|
if (!autoCalculate || formData.isComment) return
|
||||||
try {
|
const minutes = minutesBetween(formData.startTime, formData.endTime)
|
||||||
const startHour = parseInt(formData.startTime.substring(0, 2))
|
if (minutes !== null) {
|
||||||
const startMin = parseInt(formData.startTime.substring(2, 4))
|
setFormData(prev => ({ ...prev, totalTime: minutes }))
|
||||||
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])
|
}, [formData.startTime, formData.endTime, formData.isComment, autoCalculate])
|
||||||
|
|
||||||
const handleChange = (field, value) => {
|
const handleChange = (field, value) => {
|
||||||
setFormData(prev => ({ ...prev, [field]: value }))
|
setFormData(prev => ({ ...prev, [field]: value }))
|
||||||
|
|
||||||
// Wenn totalTime manuell geändert wird, deaktiviere Auto-Berechnung
|
// Wenn totalTime manuell geändert wird, deaktiviere Auto-Berechnung
|
||||||
if (field === 'totalTime') {
|
if (field === 'totalTime') {
|
||||||
setAutoCalculate(false)
|
setAutoCalculate(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Das Ende darf nicht vor dem Start liegen, deshalb rueckt es mit
|
||||||
|
const handleStartDateChange = (value) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
startDate: value,
|
||||||
|
endDate: value && prev.endDate && prev.endDate < value ? value : prev.endDate
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!formData.details.trim()) {
|
if (!formData.details.trim()) {
|
||||||
setError('Bitte Details eingeben')
|
setError('Bitte Details eingeben')
|
||||||
@@ -128,24 +102,23 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
serviceType: formData.serviceType,
|
serviceType: formData.serviceType,
|
||||||
oldStatus: workorder.status,
|
oldStatus: workorder.status,
|
||||||
newStatus: formData.newStatus,
|
newStatus: formData.newStatus,
|
||||||
oldResponseLevel: workorder.responseLevel || '',
|
|
||||||
newResponseLevel: formData.newResponseLevel,
|
|
||||||
totalTime: formData.isComment ? 0 : parseInt(formData.totalTime) || 0,
|
totalTime: formData.isComment ? 0 : parseInt(formData.totalTime) || 0,
|
||||||
startDate: formData.startDate,
|
startDate: isoToGermanDate(formData.startDate),
|
||||||
startTime: formData.startTime,
|
startTime: timeToWoms(formData.startTime),
|
||||||
endDate: formData.endDate,
|
endDate: isoToGermanDate(formData.endDate),
|
||||||
endTime: formData.endTime,
|
endTime: timeToWoms(formData.endTime),
|
||||||
details: formData.details,
|
details: formData.details,
|
||||||
isComment: formData.isComment,
|
isComment: formData.isComment,
|
||||||
|
countsToPlan: !formData.isComment && formData.countsToPlan,
|
||||||
employeeShort: user?.prefs?.shortCode || '' // Aus User-Preferences
|
employeeShort: user?.prefs?.shortCode || '' // Aus User-Preferences
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await onCreate(worksheetData, user)
|
const result = await onCreate(worksheetData, user)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onClose()
|
onClose()
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Fehler beim Erstellen des Worksheets')
|
setError(result.error || 'Fehler beim Speichern des Arbeitsblatts')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creating worksheet:', err)
|
console.error('Error creating worksheet:', err)
|
||||||
@@ -163,19 +136,19 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
<FaTimes />
|
<FaTimes />
|
||||||
</span>
|
</span>
|
||||||
<div className="overlay-content">
|
<div className="overlay-content">
|
||||||
<h2 className="mb-2">Create New Worksheet - WOID {workorder.woid}</h2>
|
<h2 className="mb-2">Neues Arbeitsblatt – Ticket {workorder.woid}</h2>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col col-6">
|
<div className="col col-6">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Service Type</label>
|
<label className="form-label">Art</label>
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
value={formData.serviceType}
|
value={formData.serviceType}
|
||||||
@@ -183,37 +156,28 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
required
|
required
|
||||||
>
|
>
|
||||||
{SERVICE_TYPES.map(type => (
|
{SERVICE_TYPES.map(type => (
|
||||||
<option key={type} value={type}>{type}</option>
|
<option key={type.value} value={type.value}>{type.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">New Status</label>
|
<label className="form-label">{isAcquisition ? 'Akquise-Ergebnis' : 'Neuer Status'}</label>
|
||||||
<select
|
<select
|
||||||
className="form-control"
|
className="form-control"
|
||||||
value={formData.newStatus}
|
value={formData.newStatus}
|
||||||
onChange={(e) => handleChange('newStatus', e.target.value)}
|
onChange={(e) => handleChange('newStatus', e.target.value)}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
{STATUS_OPTIONS.map(status => (
|
{statusOptions.map(status => (
|
||||||
<option key={status} value={status}>{status}</option>
|
<option key={status.value} value={status.value}>{status.label}</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>
|
</select>
|
||||||
|
{isAcquisition && (
|
||||||
|
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||||
|
"Zugesagt" macht den Lead automatisch zum festen Kunden.
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
@@ -226,80 +190,72 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
Nur Kommentar (keine Arbeitszeit)
|
Nur Kommentar (keine Arbeitszeit)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px' }} title="Zeit wird vom Stundenguthaben des Wartungs-Abos abgezogen und im Kundenportal angezeigt">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.countsToPlan && !formData.isComment}
|
||||||
|
disabled={formData.isComment}
|
||||||
|
onChange={(e) => handleChange('countsToPlan', e.target.checked)}
|
||||||
|
/>
|
||||||
|
Vom Abo-Stundenkonto abziehen
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col col-6">
|
<div className="col col-6">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Total Time (Minuten)</label>
|
<label className="form-label" htmlFor="worksheet-start-date">Beginn</label>
|
||||||
|
<DateTimeInput
|
||||||
|
id="worksheet-start-date"
|
||||||
|
timeLabel="Uhrzeit Beginn"
|
||||||
|
required
|
||||||
|
date={formData.startDate}
|
||||||
|
time={formData.startTime}
|
||||||
|
onDateChange={handleStartDateChange}
|
||||||
|
onTimeChange={(value) => handleChange('startTime', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="worksheet-end-date">Ende</label>
|
||||||
|
<DateTimeInput
|
||||||
|
id="worksheet-end-date"
|
||||||
|
timeLabel="Uhrzeit Ende"
|
||||||
|
required
|
||||||
|
min={formData.startDate}
|
||||||
|
date={formData.endDate}
|
||||||
|
time={formData.endTime}
|
||||||
|
onDateChange={(value) => handleChange('endDate', value)}
|
||||||
|
onTimeChange={(value) => handleChange('endTime', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label">Arbeitszeit (Minuten)</label>
|
||||||
|
{/* Ganze Minuten: mit step 15 blockierte der Browser berechnete Zeiten wie 70 */}
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
min="0"
|
min="0"
|
||||||
step="15"
|
step="1"
|
||||||
value={formData.totalTime}
|
value={formData.totalTime}
|
||||||
onChange={(e) => handleChange('totalTime', e.target.value)}
|
onChange={(e) => handleChange('totalTime', e.target.value)}
|
||||||
disabled={formData.isComment}
|
disabled={formData.isComment}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||||
{autoCalculate && formData.startTime && formData.endTime
|
{autoCalculate && formData.startTime && formData.endTime
|
||||||
? '✓ Automatisch berechnet'
|
? '✓ Automatisch berechnet'
|
||||||
: 'Manuell eingeben'}
|
: 'Manuell eingeben'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label">Action Details</label>
|
<label className="form-label">Was wurde gemacht?</label>
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control"
|
className="form-control"
|
||||||
rows={5}
|
rows={5}
|
||||||
@@ -311,12 +267,12 @@ export default function CreateWorksheetModal({ isOpen, onClose, workorder, onCre
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-dark"
|
className="btn btn-dark"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? 'Creating...' : 'CREATE NOW'}
|
{loading ? 'Speichert …' : 'Speichern'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
import { useState, useRef } from 'react'
|
|
||||||
import { FaTimes, FaSpinner } from 'react-icons/fa'
|
|
||||||
import { storage, BUCKET_ID, ID } from '../lib/appwrite'
|
|
||||||
|
|
||||||
export default function FileUploadModal({ isOpen, onClose, workorderId, onUploadComplete }) {
|
|
||||||
const [uploading, setUploading] = useState(false)
|
|
||||||
const [dragOver, setDragOver] = useState(false)
|
|
||||||
const fileInputRef = useRef(null)
|
|
||||||
|
|
||||||
const handleDrop = async (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setDragOver(false)
|
|
||||||
|
|
||||||
const file = e.dataTransfer.files[0]
|
|
||||||
if (file) {
|
|
||||||
await uploadFile(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileSelect = async (e) => {
|
|
||||||
const file = e.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
await uploadFile(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadFile = async (file) => {
|
|
||||||
// Validate file type
|
|
||||||
const allowedTypes = ['application/pdf', 'image/gif', 'image/png', 'image/jpeg']
|
|
||||||
if (!allowedTypes.includes(file.type)) {
|
|
||||||
alert('Only PDF, GIF, PNG, and JPEG files are allowed.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploading(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await storage.createFile(
|
|
||||||
BUCKET_ID,
|
|
||||||
ID.unique(),
|
|
||||||
file
|
|
||||||
)
|
|
||||||
|
|
||||||
onUploadComplete?.(response)
|
|
||||||
onClose()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload error:', error)
|
|
||||||
alert('Error uploading file: ' + error.message)
|
|
||||||
} finally {
|
|
||||||
setUploading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overlay">
|
|
||||||
<span className="overlay-close" onClick={onClose}>
|
|
||||||
<FaTimes />
|
|
||||||
</span>
|
|
||||||
<div className="overlay-content">
|
|
||||||
<h2 className="text-center mb-2">UPLOAD AREA</h2>
|
|
||||||
<p className="mb-2">Upload Attachments for WOID {workorderId}</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`drop-zone ${dragOver ? 'border-green' : ''}`}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
onDragOver={(e) => { e.preventDefault(); setDragOver(true) }}
|
|
||||||
onDragLeave={() => setDragOver(false)}
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
{uploading ? (
|
|
||||||
<div>
|
|
||||||
<FaSpinner className="spinner" size={32} />
|
|
||||||
<p className="mt-1">Uploading file...</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p>Drop file here (.pdf, .gif, .png, .jpg, .jpeg)</p>
|
|
||||||
<p className="mt-1">or</p>
|
|
||||||
<button className="btn btn-dark mt-1">Select File</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept=".pdf,.gif,.png,.jpg,.jpeg"
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
<p>
|
<p className="faint" style={{ fontSize: 13, margin: 0 }}>
|
||||||
WOMS 2.0 - Work Order Management System |
|
Webklar Ticket-Plattform · © {new Date().getFullYear()}
|
||||||
Powered by <a href="https://appwrite.io" target="_blank" rel="noopener noreferrer">Appwrite</a> & React
|
|
||||||
</p>
|
|
||||||
<p className="text-small text-grey">
|
|
||||||
© {new Date().getFullYear()} - All rights reserved
|
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { SiGitea } from 'react-icons/si'
|
||||||
|
|
||||||
|
export default function GiteaLinkButton({ href, title = 'In Gitea oeffnen', size = 16 }) {
|
||||||
|
if (!href) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: 'rgba(99, 102, 241, 0.15)',
|
||||||
|
border: '1px solid rgba(99, 102, 241, 0.35)',
|
||||||
|
color: '#818cf8',
|
||||||
|
textDecoration: 'none',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SiGitea size={size} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { FaFilePdf, FaPlus, FaLink, FaSpinner, FaRotate } from 'react-icons/fa6'
|
||||||
|
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
||||||
|
import { integrationsApi, formatMoney, INVOICE_STATUS } from '../lib/integrationsApi'
|
||||||
|
import { useInvoices } from '../hooks/useInvoices'
|
||||||
|
import { customerStage, isInvoiceReady } from '../lib/leads'
|
||||||
|
|
||||||
|
const card = {
|
||||||
|
background: 'rgba(45, 55, 72, 0.5)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '16px',
|
||||||
|
border: '1px solid rgba(16, 185, 129, 0.2)',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InvoicePanel({ ticket }) {
|
||||||
|
const [customer, setCustomer] = useState(null)
|
||||||
|
const [customerLoading, setCustomerLoading] = useState(true)
|
||||||
|
const [customerError, setCustomerError] = useState(null)
|
||||||
|
|
||||||
|
const loadCustomer = useCallback(async () => {
|
||||||
|
if (!ticket.customerId) {
|
||||||
|
setCustomer(null)
|
||||||
|
setCustomerLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCustomerLoading(true)
|
||||||
|
try {
|
||||||
|
const doc = await databases.getDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, ticket.customerId)
|
||||||
|
setCustomer(doc)
|
||||||
|
setCustomerError(null)
|
||||||
|
} catch (err) {
|
||||||
|
setCustomerError(err.message)
|
||||||
|
setCustomer(null)
|
||||||
|
} finally {
|
||||||
|
setCustomerLoading(false)
|
||||||
|
}
|
||||||
|
}, [ticket.customerId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCustomer()
|
||||||
|
}, [loadCustomer])
|
||||||
|
|
||||||
|
const clientId = customer?.invoiceNinjaClientId || null
|
||||||
|
const { invoices, loading, error, refresh } = useInvoices(clientId)
|
||||||
|
|
||||||
|
if (customerLoading) {
|
||||||
|
return (
|
||||||
|
<div style={card}>
|
||||||
|
<FaSpinner className="spinner" /> Kunde wird geladen...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ticket.customerId) {
|
||||||
|
return (
|
||||||
|
<div style={card}>
|
||||||
|
<p style={{ color: '#a0aec0', margin: 0 }}>Kein Kunde am Ticket verknüpft, keine Rechnungen verfügbar.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={card}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||||
|
<h5 style={{ color: 'var(--dark-text)', fontWeight: 'bold', margin: 0, fontSize: '16px' }}>
|
||||||
|
Rechnungen {customer?.name ? `- ${customer.name}` : ''}
|
||||||
|
</h5>
|
||||||
|
{clientId && (
|
||||||
|
<button className="btn" onClick={refresh} title="Aktualisieren"
|
||||||
|
style={{ background: '#374151', color: 'white', border: 'none', padding: '6px 10px', borderRadius: '6px', cursor: 'pointer' }}>
|
||||||
|
<FaRotate />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{customerError && <div className="text-red" style={{ marginBottom: 8 }}>{customerError}</div>}
|
||||||
|
|
||||||
|
{customer && customerStage(customer) === 'lead' ? (
|
||||||
|
<p style={{ color: '#a0aec0', margin: 0 }}>
|
||||||
|
Dieser Kunde ist noch ein Lead. Erst zum festen Kunden machen (im Kunden-Bereich, vollständige Daten nötig), dann sind Rechnungen möglich.
|
||||||
|
</p>
|
||||||
|
) : !clientId && !isInvoiceReady(customer) ? (
|
||||||
|
<p style={{ color: '#a0aec0', margin: 0 }}>
|
||||||
|
Rechnungsdaten unvollständig. Bitte im Kunden-Bereich Unternehmen, E-Mail, Straße, PLZ und Stadt ergänzen.
|
||||||
|
</p>
|
||||||
|
) : !clientId ? (
|
||||||
|
<LinkClient customer={customer} onLinked={loadCustomer} />
|
||||||
|
) : (
|
||||||
|
<InvoiceList invoices={invoices} loading={loading} error={error} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------- Verknuepfung -----------
|
||||||
|
function LinkClient({ customer, onLinked }) {
|
||||||
|
const [search, setSearch] = useState(customer?.name || '')
|
||||||
|
const [results, setResults] = useState([])
|
||||||
|
const [busy, setBusy] = useState(false)
|
||||||
|
const [error, setError] = useState(null)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
|
const doSearch = async () => {
|
||||||
|
setBusy(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const data = await integrationsApi.listClients(search)
|
||||||
|
setResults(data.clients || [])
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message)
|
||||||
|
} finally {
|
||||||
|
setBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
doSearch()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const saveMapping = async (clientId) => {
|
||||||
|
setSaving(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, {
|
||||||
|
invoiceNinjaClientId: clientId,
|
||||||
|
})
|
||||||
|
onLinked()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message)
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAndLink = async () => {
|
||||||
|
setSaving(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const data = await integrationsApi.createClient({
|
||||||
|
name: customer.name || customer.companyName || 'Neuer Kunde',
|
||||||
|
email: customer.email || '',
|
||||||
|
phone: customer.phone || '',
|
||||||
|
address: customer.location || '',
|
||||||
|
})
|
||||||
|
if (data.client?.id) {
|
||||||
|
await saveMapping(data.client.id)
|
||||||
|
} else {
|
||||||
|
throw new Error('Client konnte nicht angelegt werden')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message)
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p style={{ color: '#a0aec0', fontSize: '13px', marginTop: 0 }}>
|
||||||
|
Dieser Kunde ist noch nicht mit InvoiceNinja verknüpft.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '10px' }}>
|
||||||
|
<input
|
||||||
|
className="form-control"
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
placeholder="InvoiceNinja-Kunde suchen …"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && doSearch()}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-green" style={{ margin: 0 }} onClick={doSearch} disabled={busy}>
|
||||||
|
{busy ? <FaSpinner className="spinner" /> : 'Suchen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
|
||||||
|
|
||||||
|
<div style={{ maxHeight: '180px', overflowY: 'auto', marginBottom: '10px' }}>
|
||||||
|
{results.map((c) => (
|
||||||
|
<div key={c.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px', borderBottom: '1px solid rgba(255,255,255,0.06)' }}>
|
||||||
|
<div>
|
||||||
|
<strong style={{ color: 'var(--dark-text)' }}>{c.name}</strong>
|
||||||
|
{c.number ? <span style={{ color: '#a0aec0' }}> · {c.number}</span> : null}
|
||||||
|
{c.email ? <div style={{ color: '#a0aec0', fontSize: '12px' }}>{c.email}</div> : null}
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-green" style={{ margin: 0 }} disabled={saving} onClick={() => saveMapping(c.id)}>
|
||||||
|
<FaLink /> Verknüpfen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!busy && results.length === 0 && (
|
||||||
|
<p style={{ color: '#a0aec0', fontSize: '13px' }}>Keine Treffer.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn" style={{ background: '#3b82f6', color: 'white', border: 'none', padding: '10px 16px', borderRadius: '8px', cursor: 'pointer' }} disabled={saving} onClick={createAndLink}>
|
||||||
|
{saving ? <FaSpinner className="spinner" /> : <><FaPlus /> Neuen InvoiceNinja-Kunden anlegen & verknüpfen</>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------- Rechnungsliste ---------
|
||||||
|
// Nur anzeigen: neue Rechnungen entstehen direkt in InvoiceNinja
|
||||||
|
function InvoiceList({ invoices, loading, error }) {
|
||||||
|
const [openingId, setOpeningId] = useState(null)
|
||||||
|
|
||||||
|
const openPdf = async (id) => {
|
||||||
|
setOpeningId(id)
|
||||||
|
try {
|
||||||
|
await integrationsApi.openInvoicePdf(id)
|
||||||
|
} catch (err) {
|
||||||
|
alert('PDF konnte nicht geöffnet werden: ' + err.message)
|
||||||
|
} finally {
|
||||||
|
setOpeningId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<p style={{ color: '#a0aec0' }}><FaSpinner className="spinner" /> Rechnungen werden geladen...</p>
|
||||||
|
) : invoices.length === 0 ? (
|
||||||
|
<p style={{ color: '#a0aec0', fontSize: '13px' }}>Noch keine Rechnungen für diesen Kunden.</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ overflowX: 'auto' }}>
|
||||||
|
<table className="table" style={{ width: '100%', fontSize: '13px' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ color: '#a0aec0', textAlign: 'left' }}>
|
||||||
|
<th>Nr.</th><th>Datum</th><th>Fällig</th><th>Betrag</th><th>Offen</th><th>Status</th><th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invoices.map((inv) => {
|
||||||
|
const st = INVOICE_STATUS[inv.status_id] || { label: '–', color: '#a0aec0' }
|
||||||
|
const overdue = Number(inv.balance) > 0 && inv.due_date && new Date(inv.due_date) < new Date()
|
||||||
|
return (
|
||||||
|
<tr key={inv.id}>
|
||||||
|
<td style={{ color: 'var(--dark-text)' }}>{inv.number || inv.id?.slice(-6)}</td>
|
||||||
|
<td style={{ color: '#cbd5e0' }}>{inv.date || '-'}</td>
|
||||||
|
<td style={{ color: overdue ? '#ef4444' : '#cbd5e0' }}>{inv.due_date || '-'}</td>
|
||||||
|
<td style={{ color: 'var(--dark-text)' }}>{formatMoney(inv.amount)}</td>
|
||||||
|
<td style={{ color: Number(inv.balance) > 0 ? '#f59e0b' : '#10b981' }}>{formatMoney(inv.balance)}</td>
|
||||||
|
<td><span style={{ color: st.color, fontWeight: 'bold' }}>{st.label}{overdue ? ' (überfällig)' : ''}</span></td>
|
||||||
|
<td>
|
||||||
|
<button className="btn" title="PDF öffnen" disabled={openingId === inv.id}
|
||||||
|
style={{ background: '#ef4444', color: 'white', border: 'none', padding: '6px 10px', borderRadius: '6px', cursor: 'pointer' }}
|
||||||
|
onClick={() => openPdf(inv.id)}>
|
||||||
|
{openingId === inv.id ? <FaSpinner className="spinner" /> : <FaFilePdf />}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
/* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
+81
-144
@@ -1,174 +1,111 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { motion } from 'motion/react'
|
import {
|
||||||
import {
|
IconTicket,
|
||||||
IconTicket,
|
IconUsers,
|
||||||
IconDeviceDesktop,
|
IconTargetArrow,
|
||||||
IconChartBar,
|
IconReportMoney,
|
||||||
IconCalendar,
|
IconFolders,
|
||||||
IconFolders,
|
IconCalendar,
|
||||||
IconFileText,
|
IconRoute,
|
||||||
IconBook,
|
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconLogout
|
IconLogout,
|
||||||
|
IconLayoutSidebarLeftCollapse,
|
||||||
|
IconLayoutSidebarLeftExpand,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { Sidebar, SidebarBody, SidebarLink } from './ModernSidebar'
|
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Sidebar() {
|
||||||
const { user, logout, isAdmin } = useAuth()
|
const { user, logout, isAdmin } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [open, setOpen] = useState(false)
|
const location = useLocation()
|
||||||
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
const links = [
|
const groups = [
|
||||||
{
|
{
|
||||||
label: 'Tickets',
|
label: 'Betrieb',
|
||||||
href: '/tickets',
|
links: [
|
||||||
icon: <IconTicket className="icon" />,
|
{ label: 'Tickets', href: '/tickets', icon: <IconTicket /> },
|
||||||
|
{ label: 'Planboard', href: '/planboard', icon: <IconCalendar /> },
|
||||||
|
{ label: 'Roadmaps', href: '/roadmaps', icon: <IconRoute /> },
|
||||||
|
{ label: 'Projekte', href: '/projects', icon: <IconFolders /> },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Assets',
|
label: 'CRM',
|
||||||
href: '/assets',
|
links: [
|
||||||
icon: <IconDeviceDesktop className="icon" />,
|
{ label: 'Kunden', href: '/customers', icon: <IconUsers /> },
|
||||||
|
{ label: 'Leads', href: '/leads', icon: <IconTargetArrow /> },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Dashboard',
|
label: 'Finanzen',
|
||||||
href: '/dashboard',
|
links: [{ label: 'Finanzen', href: '/finance', icon: <IconReportMoney /> }],
|
||||||
icon: <IconChartBar className="icon" />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Planboard',
|
label: 'System',
|
||||||
href: '/planboard',
|
links: [
|
||||||
icon: <IconCalendar className="icon" />,
|
...(isAdmin ? [{ label: 'Admin', href: '/admin', icon: <IconSettings /> }] : []),
|
||||||
},
|
],
|
||||||
{
|
|
||||||
label: 'Projects',
|
|
||||||
href: '/projects',
|
|
||||||
icon: <IconFolders className="icon" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Reports',
|
|
||||||
href: '/reports',
|
|
||||||
icon: <IconFileText className="icon" />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Docs',
|
|
||||||
href: '/docs',
|
|
||||||
icon: <IconBook className="icon" />,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Admin link nur wenn isAdmin
|
const isActive = (href) =>
|
||||||
if (isAdmin) {
|
location.pathname === href || location.pathname.startsWith(href + '/')
|
||||||
links.push({
|
|
||||||
label: 'Admin',
|
|
||||||
href: '/admin',
|
|
||||||
icon: <IconSettings className="icon" />,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar open={open} setOpen={setOpen}>
|
<aside className={`side ${collapsed ? 'collapsed' : ''}`}>
|
||||||
<SidebarBody className="justify-between gap-10">
|
<div className="side-head">
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowX: 'hidden', overflowY: 'auto' }}>
|
<Link to="/" className="side-logo" title="Webklar">W</Link>
|
||||||
<Logo />
|
<div className="side-brand">
|
||||||
<div style={{ marginTop: '2rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
Webklar
|
||||||
{links.map((link, idx) => (
|
<small>Ticket-Plattform</small>
|
||||||
<SidebarLink key={idx} link={link} />
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="side-nav">
|
||||||
|
{/* Gruppen ohne Eintrag (System fuer Nicht-Admins) nicht anzeigen */}
|
||||||
|
{groups.filter((group) => group.links.length > 0).map((group) => (
|
||||||
|
<div className="side-group" key={group.label}>
|
||||||
|
<div className="side-group-label">{group.label}</div>
|
||||||
|
{group.links.map((link) => (
|
||||||
|
<Link
|
||||||
|
key={link.href}
|
||||||
|
to={link.href}
|
||||||
|
className={`side-link ${isActive(link.href) ? 'active' : ''}`}
|
||||||
|
title={link.label}
|
||||||
|
>
|
||||||
|
{link.icon}
|
||||||
|
<span className="side-link-label">{link.label}</span>
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
<div>
|
</nav>
|
||||||
{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 = () => {
|
<div className="side-foot">
|
||||||
return (
|
{user && (
|
||||||
<Link
|
<div className="side-link" title={user.name || user.email} style={{ cursor: 'default' }}>
|
||||||
to="/"
|
<div className="side-logo" style={{ width: 28, height: 28, fontSize: 12, background: 'linear-gradient(135deg,#667eea,#764ba2)' }}>
|
||||||
style={{
|
{(user.name || user.email || '?').charAt(0).toUpperCase()}
|
||||||
position: 'relative',
|
</div>
|
||||||
zIndex: 20,
|
<span className="side-link-label text-ellipsis">{user.name || user.email}</span>
|
||||||
display: 'flex',
|
</div>
|
||||||
alignItems: 'center',
|
)}
|
||||||
gap: '0.5rem',
|
<button className="side-toggle" onClick={handleLogout} title="Logout">
|
||||||
padding: '0.25rem 0',
|
<IconLogout size={20} />
|
||||||
fontSize: '0.875rem',
|
<span className="side-link-label">Logout</span>
|
||||||
fontWeight: 'normal',
|
</button>
|
||||||
color: 'var(--text-color, #333)',
|
<button className="side-toggle" onClick={() => setCollapsed((c) => !c)} title="Menue ein-/ausklappen">
|
||||||
textDecoration: 'none'
|
{collapsed ? <IconLayoutSidebarLeftExpand size={20} /> : <IconLayoutSidebarLeftCollapse size={20} />}
|
||||||
}}
|
<span className="side-link-label">Einklappen</span>
|
||||||
>
|
</button>
|
||||||
<div style={{
|
</div>
|
||||||
height: '20px',
|
</aside>
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.pixel-blast-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,674 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { FaEye } from 'react-icons/fa6'
|
||||||
|
|
||||||
|
export default function PreviewLinkButton({ href, title = 'Preview oeffnen', size = 16 }) {
|
||||||
|
if (!href) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: 'rgba(16, 185, 129, 0.15)',
|
||||||
|
border: '1px solid rgba(16, 185, 129, 0.35)',
|
||||||
|
color: '#34d399',
|
||||||
|
textDecoration: 'none',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaEye size={size} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
const PRIORITIES = [
|
|
||||||
{ value: 0, label: 'None' },
|
|
||||||
{ value: 1, label: 'Low' },
|
|
||||||
{ value: 2, label: 'Medium' },
|
|
||||||
{ value: 3, label: 'High' },
|
|
||||||
{ value: 4, label: 'Critical' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const PRIORITY_LABELS = {
|
|
||||||
0: 'NONE',
|
|
||||||
1: 'LOW',
|
|
||||||
2: 'MEDIUM',
|
|
||||||
3: 'HIGH',
|
|
||||||
4: 'CRITICAL'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PriorityDropdown({ value, onChange }) {
|
|
||||||
return (
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
|
|
||||||
{PRIORITY_LABELS[value] || 'LOW'}
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-content">
|
|
||||||
{PRIORITIES.map(priority => (
|
|
||||||
<span
|
|
||||||
key={priority.value}
|
|
||||||
className="dropdown-item"
|
|
||||||
onClick={() => onChange(priority.value)}
|
|
||||||
>
|
|
||||||
{priority.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { IconFolders } from '@tabler/icons-react'
|
||||||
|
import { StatusPill } from './ui/StatusPill'
|
||||||
|
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
|
||||||
|
import { fetchRoadmapByTicketId } from '../lib/roadmaps'
|
||||||
|
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projekt-Kontextbox im Ticket: zeigt automatisch das zugehoerige Projekt,
|
||||||
|
* alle anderen Tickets desselben Kunden (klickbar) und den Link zum aktiven Plan.
|
||||||
|
* Rendert nichts, wenn das Ticket keinen Projektbezug hat.
|
||||||
|
*/
|
||||||
|
export default function ProjectContextPanel({ ticket }) {
|
||||||
|
const { fetchByTicketId } = useWebsiteProjects()
|
||||||
|
const [context, setContext] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
// Projektbezug: Roadmap dieses Tickets oder direkt zugewiesenes Projekt
|
||||||
|
const [roadmap, assignedProjects] = await Promise.all([
|
||||||
|
fetchRoadmapByTicketId(ticket.$id).catch(() => null),
|
||||||
|
fetchByTicketId(ticket.$id),
|
||||||
|
])
|
||||||
|
const projectName = roadmap?.projectName || assignedProjects[0]?.projectName || ''
|
||||||
|
if (!roadmap && !assignedProjects.length) {
|
||||||
|
if (!cancelled) setContext(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let otherTickets = []
|
||||||
|
if (ticket.customerId) {
|
||||||
|
const res = await databases.listDocuments(DATABASE_ID, COLLECTIONS.WORKORDERS, [
|
||||||
|
Query.equal('customerId', ticket.customerId),
|
||||||
|
Query.orderDesc('$createdAt'),
|
||||||
|
Query.limit(50),
|
||||||
|
])
|
||||||
|
otherTickets = res.documents.filter((t) => t.$id !== ticket.$id)
|
||||||
|
}
|
||||||
|
if (!cancelled) setContext({ roadmap, projectName, otherTickets })
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setContext(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [ticket.$id, ticket.customerId, fetchByTicketId])
|
||||||
|
|
||||||
|
if (!context) return null
|
||||||
|
const { roadmap, projectName, otherTickets } = context
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ui-card pad" style={{ marginTop: 16 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||||
|
<h5 style={{ fontWeight: 700, margin: 0, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<IconFolders size={18} /> Projekt: {projectName || '-'}
|
||||||
|
</h5>
|
||||||
|
{roadmap && (
|
||||||
|
<Link to={`/roadmaps/${roadmap.$id}`} className="ui-btn">
|
||||||
|
Plan: {roadmap.title}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!ticket.customerId ? (
|
||||||
|
<p className="faint" style={{ fontSize: 13, margin: 0 }}>
|
||||||
|
Kein Kunde zugeordnet — andere Tickets koennen nicht automatisch verknuepft werden.
|
||||||
|
</p>
|
||||||
|
) : !otherTickets.length ? (
|
||||||
|
<p className="faint" style={{ fontSize: 13, margin: 0 }}>
|
||||||
|
Keine weiteren Tickets zu diesem Kunden/Projekt.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
{otherTickets.map((t) => (
|
||||||
|
<Link
|
||||||
|
key={t.$id}
|
||||||
|
to={`/tickets?woid=${t.woid}`}
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 10, fontSize: 13,
|
||||||
|
padding: '6px 8px', borderRadius: 8, border: '1px solid var(--border)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="muted" style={{ minWidth: 48, fontWeight: 700 }}>{t.woid}</span>
|
||||||
|
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{t.topic || t.title || '-'}
|
||||||
|
</span>
|
||||||
|
<StatusPill status={t.status} />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { FaFileLines } from 'react-icons/fa6'
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import { fetchProjectReadme } from '../lib/projectAdminApi'
|
||||||
|
|
||||||
|
const markdownComponents = {
|
||||||
|
h1: ({ children }) => <h1 style={{ fontSize: '1.35rem', fontWeight: 700, margin: '0 0 10px', color: '#f8fafc' }}>{children}</h1>,
|
||||||
|
h2: ({ children }) => <h2 style={{ fontSize: '1.15rem', fontWeight: 700, margin: '14px 0 8px', color: '#f1f5f9' }}>{children}</h2>,
|
||||||
|
h3: ({ children }) => <h3 style={{ fontSize: '1rem', fontWeight: 600, margin: '12px 0 6px', color: '#e2e8f0' }}>{children}</h3>,
|
||||||
|
p: ({ children }) => <p style={{ margin: '0 0 10px', lineHeight: 1.6 }}>{children}</p>,
|
||||||
|
ul: ({ children }) => <ul style={{ margin: '0 0 10px', paddingLeft: 20 }}>{children}</ul>,
|
||||||
|
ol: ({ children }) => <ol style={{ margin: '0 0 10px', paddingLeft: 20 }}>{children}</ol>,
|
||||||
|
code: ({ inline, children }) => inline ? (
|
||||||
|
<code style={{ background: 'rgba(30,41,59,0.9)', padding: '2px 6px', borderRadius: 4, fontSize: '0.9em' }}>{children}</code>
|
||||||
|
) : <code>{children}</code>,
|
||||||
|
pre: ({ children }) => <pre style={{ background: 'rgba(15,23,42,0.95)', borderRadius: 6, padding: 10, overflowX: 'auto', margin: '0 0 10px' }}>{children}</pre>,
|
||||||
|
a: ({ href, children }) => <a href={href} target="_blank" rel="noopener noreferrer" style={{ color: '#60a5fa' }}>{children}</a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectReadme({ repoFullName }) {
|
||||||
|
const [state, setState] = useState({ loading: true, content: '', filename: '', error: '' })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!repoFullName) { setState({ loading: false, content: '', filename: '', error: '' }); return }
|
||||||
|
let cancelled = false
|
||||||
|
setState({ loading: true, content: '', filename: '', error: '' })
|
||||||
|
fetchProjectReadme(repoFullName)
|
||||||
|
.then((data) => {
|
||||||
|
if (cancelled) return
|
||||||
|
if (!data.found) { setState({ loading: false, content: '', filename: '', error: '' }); return }
|
||||||
|
setState({ loading: false, content: data.content || '', filename: data.filename || 'README.md', error: '' })
|
||||||
|
})
|
||||||
|
.catch((err) => { if (!cancelled) setState({ loading: false, content: '', filename: '', error: err.message }) })
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [repoFullName])
|
||||||
|
|
||||||
|
if (state.loading) return <p className="text-grey" style={{ fontSize: 13, marginTop: 8 }}>README wird geladen...</p>
|
||||||
|
if (state.error || !state.content) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6, fontSize: 13, color: '#94a3b8' }}>
|
||||||
|
<FaFileLines /><span>{state.filename}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(148,163,184,0.2)', borderRadius: 8, padding: '12px 14px', fontSize: 13, color: '#e2e8f0', maxHeight: 320, overflowY: 'auto' }}>
|
||||||
|
<ReactMarkdown components={markdownComponents}>{state.content}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { FaTimes } from 'react-icons/fa'
|
|
||||||
import { format } from 'date-fns'
|
|
||||||
|
|
||||||
const STATUS_CLASSES = {
|
|
||||||
'Open': 'bg-red',
|
|
||||||
'Occupied': 'bg-blue-grey',
|
|
||||||
'Assigned': 'bg-teal',
|
|
||||||
'Awaiting': 'bg-amber',
|
|
||||||
'Closed': 'bg-grey'
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRIORITY_CLASSES = {
|
|
||||||
0: 'bg-blue',
|
|
||||||
1: 'bg-green',
|
|
||||||
2: 'bg-amber',
|
|
||||||
3: 'bg-orange',
|
|
||||||
4: 'bg-red'
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRIORITY_LABELS = {
|
|
||||||
0: 'NONE',
|
|
||||||
1: 'LOW',
|
|
||||||
2: 'MEDIUM',
|
|
||||||
3: 'HIGH',
|
|
||||||
4: 'CRITICAL'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function QuickOverviewModal({ isOpen, onClose, workorders = [] }) {
|
|
||||||
if (!isOpen) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overlay">
|
|
||||||
<span className="overlay-close" onClick={onClose}>
|
|
||||||
<FaTimes />
|
|
||||||
</span>
|
|
||||||
<div className="overlay-content">
|
|
||||||
<h3 className="mb-2">WOMS Ticket Quick Overview</h3>
|
|
||||||
|
|
||||||
<table className="table" style={{ background: '#fff', color: '#000' }}>
|
|
||||||
<thead>
|
|
||||||
<tr className="bg-dark-grey text-white">
|
|
||||||
<th>WOID</th>
|
|
||||||
<th>Created</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>System</th>
|
|
||||||
<th>CID & CLO</th>
|
|
||||||
<th>Requested by</th>
|
|
||||||
<th>Topic</th>
|
|
||||||
<th className="text-center">Priority</th>
|
|
||||||
<th className="text-right">Status</th>
|
|
||||||
<th>Editor</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{workorders.map(wo => (
|
|
||||||
<tr key={wo.$id} className="bg-white">
|
|
||||||
<td className="bg-dark-grey text-white text-small">
|
|
||||||
{wo.woid || wo.$id?.slice(-5)}
|
|
||||||
</td>
|
|
||||||
<td className="bg-light-grey text-small">
|
|
||||||
{format(new Date(wo.$createdAt || wo.createdAt), 'dd.MM.yyyy, HH:mm')}h
|
|
||||||
</td>
|
|
||||||
<td className="bg-light-grey text-small">{wo.type}</td>
|
|
||||||
<td className="bg-light-grey text-small">{wo.systemType || 'n/a'}</td>
|
|
||||||
<td className="text-small">{wo.customerCode} {wo.customerLocation}</td>
|
|
||||||
<td className="text-small">{wo.requestedBy}</td>
|
|
||||||
<td className="text-small">{wo.topic}</td>
|
|
||||||
<td className={`text-small text-center ${PRIORITY_CLASSES[wo.priority]}`}>
|
|
||||||
{PRIORITY_LABELS[wo.priority]}
|
|
||||||
</td>
|
|
||||||
<td className={`text-small text-right ${STATUS_CLASSES[wo.status]}`}>
|
|
||||||
{wo.status?.toUpperCase()}
|
|
||||||
</td>
|
|
||||||
<td className={`text-small ${STATUS_CLASSES[wo.status]}`}>
|
|
||||||
{wo.assignedTo || '-'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p className="mt-2 text-center">
|
|
||||||
Summary: Listed a total of {workorders.length} Workorders!
|
|
||||||
<br />EOL =)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const RESPONSE_LEVELS = [
|
|
||||||
{ value: 'KEY USER', label: 'Key User' },
|
|
||||||
{ value: 'Helpdesk', label: '1st Level' },
|
|
||||||
{ value: 'Support', label: '2nd Level' },
|
|
||||||
{ value: 'Admin', label: '3rd Level' },
|
|
||||||
{ value: 'FS/FE', label: 'FS/FE' },
|
|
||||||
{ value: '24/7', label: '24/7' },
|
|
||||||
{ value: 'TECH MGMT', label: 'Tech Mgmt' },
|
|
||||||
{ value: 'Backoffice', label: 'Backoffice' },
|
|
||||||
{ value: 'BUSI MGMT', label: 'Busi Mgmt' }
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function ResponseDropdown({ value, onChange }) {
|
|
||||||
return (
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
|
|
||||||
{value?.toUpperCase() || 'BACKOFFICE'}
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-content">
|
|
||||||
{RESPONSE_LEVELS.map(level => (
|
|
||||||
<span
|
|
||||||
key={level.value}
|
|
||||||
className="dropdown-item"
|
|
||||||
onClick={() => onChange(level.value)}
|
|
||||||
>
|
|
||||||
{level.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,19 @@
|
|||||||
const STATUSES = [
|
import { TICKET_STATUSES, statusLabel } from '../lib/ticketStatus'
|
||||||
'Open',
|
|
||||||
'Occupied',
|
|
||||||
'Assigned',
|
|
||||||
'Awaiting',
|
|
||||||
'Added Info',
|
|
||||||
'In Test',
|
|
||||||
'Halted',
|
|
||||||
'Aborted',
|
|
||||||
'Cancelled',
|
|
||||||
'Closed'
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function StatusDropdown({ value, onChange }) {
|
export default function StatusDropdown({ value, onChange }) {
|
||||||
return (
|
return (
|
||||||
<div className="dropdown">
|
<div className="dropdown">
|
||||||
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
|
<button className="btn" style={{ background: 'inherit', color: 'inherit' }}>
|
||||||
{value?.toUpperCase() || 'OPEN'}
|
{statusLabel(value)}
|
||||||
</button>
|
</button>
|
||||||
<div className="dropdown-content">
|
<div className="dropdown-content">
|
||||||
{STATUSES.map(status => (
|
{TICKET_STATUSES.map(status => (
|
||||||
<span
|
<span
|
||||||
key={status}
|
key={status.value}
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => onChange(status)}
|
onClick={() => onChange(status.value)}
|
||||||
>
|
>
|
||||||
{status}
|
{status.label}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||||
|
import AssignedProjectCard from './AssignedProjectCard'
|
||||||
|
|
||||||
|
export default function TicketAssignedProjectsPanel({ ticket, refreshKey = 0 }) {
|
||||||
|
const { fetchByTicketId } = useWebsiteProjects()
|
||||||
|
const [projects, setProjects] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const loadProjects = useCallback(async () => {
|
||||||
|
if (!ticket?.$id) return
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
setProjects(await fetchByTicketId(ticket.$id))
|
||||||
|
} catch {
|
||||||
|
setProjects([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [ticket?.$id, fetchByTicketId])
|
||||||
|
|
||||||
|
useEffect(() => { loadProjects() }, [loadProjects, refreshKey])
|
||||||
|
|
||||||
|
if (loading || projects.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: 'rgba(45,55,72,0.5)', borderRadius: 12, padding: 20, border: '1px solid rgba(59,130,246,0.3)', marginTop: 20 }}>
|
||||||
|
<h5 style={{ fontWeight: 'bold', marginBottom: 12 }}>Zugewiesene Projekte ({projects.length})</h5>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<AssignedProjectCard key={project.$id} project={project} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { FaChevronDown, FaPlay, FaStop, FaPlus, FaPen } from 'react-icons/fa6'
|
||||||
|
import { formatDistanceToNow, format } from 'date-fns'
|
||||||
|
import { de } from 'date-fns/locale'
|
||||||
|
import StatusDropdown from './StatusDropdown'
|
||||||
|
import UrgentToggle from './UrgentToggle'
|
||||||
|
import EditorDropdown from './EditorDropdown'
|
||||||
|
import CreateWorksheetModal from './CreateWorksheetModal'
|
||||||
|
import WorksheetList from './WorksheetList'
|
||||||
|
import WorksheetTimesModal from './WorksheetTimesModal'
|
||||||
|
import WorksheetSummary from './WorksheetSummary'
|
||||||
|
import TicketAssignedProjectsPanel from './TicketAssignedProjectsPanel'
|
||||||
|
import TicketProjectsModal from './TicketProjectsModal'
|
||||||
|
import TicketRoadmapPanel from './TicketRoadmapPanel'
|
||||||
|
import ProjectContextPanel from './ProjectContextPanel'
|
||||||
|
import InvoicePanel from './InvoicePanel'
|
||||||
|
import Tabs from './ui/Tabs'
|
||||||
|
import Badge from './ui/Badge'
|
||||||
|
import { StatusPill, UrgentPill } from './ui/StatusPill'
|
||||||
|
import { useWorksheets } from '../hooks/useWorksheets'
|
||||||
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
import { databases, DATABASE_ID, COLLECTIONS } from '../lib/appwrite'
|
||||||
|
import { canEditWorksheetTimes } from '../lib/ticketForm'
|
||||||
|
import { ticketTypeLabel } from '../lib/ticketStatus'
|
||||||
|
|
||||||
|
// Projekt / Preview steht mit im Details-Reiter
|
||||||
|
const TABS = [
|
||||||
|
{ id: 'details', label: 'Details' },
|
||||||
|
{ id: 'worksheets', label: 'Arbeitsblätter' },
|
||||||
|
{ id: 'invoices', label: 'Rechnungen' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function TicketCard({ ticket, onUpdate }) {
|
||||||
|
const { user, employee } = useAuth()
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState('details')
|
||||||
|
const [showCreateWorksheet, setShowCreateWorksheet] = useState(false)
|
||||||
|
const [editTimesWorksheet, setEditTimesWorksheet] = useState(null)
|
||||||
|
const [showProjectsModal, setShowProjectsModal] = useState(false)
|
||||||
|
const [projectsRefreshKey, setProjectsRefreshKey] = useState(0)
|
||||||
|
|
||||||
|
const { worksheets, loading: worksheetsLoading, createWorksheet, updateWorksheet, updateWorksheetTimes, getTotalTime } = useWorksheets(
|
||||||
|
expanded ? ticket.woid : null
|
||||||
|
)
|
||||||
|
|
||||||
|
const createdAt = new Date(ticket.$createdAt || ticket.createdAt)
|
||||||
|
const elapsed = formatDistanceToNow(createdAt, { locale: de, addSuffix: true })
|
||||||
|
|
||||||
|
const stop = (e) => e.stopPropagation()
|
||||||
|
|
||||||
|
// Abo-Stunden: Arbeitszeit vom Stundenguthaben des Wartungs-Abos abziehen oder nicht
|
||||||
|
const handleTogglePlan = (ws) => updateWorksheet(ws.$id, { countsToPlan: !ws.countsToPlan })
|
||||||
|
|
||||||
|
const handleCreateWorksheet = async (worksheetData, currentUser) => {
|
||||||
|
const result = await createWorksheet(worksheetData, currentUser)
|
||||||
|
if (result.success && worksheetData.newStatus !== ticket.status) {
|
||||||
|
await onUpdate(ticket.$id, { status: worksheetData.newStatus })
|
||||||
|
}
|
||||||
|
// Akquise-Automatik: Ergebnis im Worksheet steuert den Kundenstatus
|
||||||
|
if (result.success && ticket.type === 'Akquise' && ticket.customerId) {
|
||||||
|
const outcome = worksheetData.newStatus
|
||||||
|
const next = outcome === 'Zugesagt' ? 'customer' : outcome === 'Abgesagt' ? 'lost' : null
|
||||||
|
if (next) {
|
||||||
|
try {
|
||||||
|
await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, ticket.customerId, {
|
||||||
|
customerStatus: next,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
} catch { /* nicht kritisch */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`tcard ${expanded ? 'open' : ''}`}>
|
||||||
|
<div className="tcard-main" onClick={() => setExpanded((e) => !e)}>
|
||||||
|
<div className="tcard-woid">
|
||||||
|
<span className="id">{ticket.woid || ticket.$id?.slice(-5)}</span>
|
||||||
|
<span className="age">{elapsed}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tcard-body">
|
||||||
|
<div className="tcard-row1">
|
||||||
|
<span className="tcard-customer">{ticket.customerName || 'Unbekannt'}</span>
|
||||||
|
{ticket.customerLocation && <span className="tcard-loc">· {ticket.customerLocation}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="tcard-topic">{ticket.topic || ticket.title || 'Kein Betreff'}</div>
|
||||||
|
<div className="tcard-badges">
|
||||||
|
<StatusPill status={ticket.status} />
|
||||||
|
<UrgentPill priority={ticket.priority} />
|
||||||
|
{ticket.type && <Badge tone="muted">{ticketTypeLabel(ticket.type)}</Badge>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tcard-side" onClick={stop}>
|
||||||
|
<div style={{ minWidth: 120 }}>
|
||||||
|
<StatusDropdown value={ticket.status} onChange={(v) => onUpdate(ticket.$id, { status: v })} />
|
||||||
|
</div>
|
||||||
|
<UrgentToggle value={ticket.priority} onChange={(v) => onUpdate(ticket.$id, { priority: v })} />
|
||||||
|
<FaChevronDown className={`chevron ${expanded ? 'up' : ''}`} onClick={() => setExpanded((e) => !e)} style={{ cursor: 'pointer' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div className="tcard-detail">
|
||||||
|
<div className="flex gap-3 wrap" style={{ marginBottom: 16, fontSize: 13 }}>
|
||||||
|
<span className="muted"><FaPlay style={{ color: 'var(--ok)' }} /> {ticket.startDate || format(createdAt, 'dd.MM.yyyy')}</span>
|
||||||
|
<span className="muted"><FaStop style={{ color: 'var(--danger)' }} /> {ticket.deadline || '-'}</span>
|
||||||
|
{ticket.requestedBy && <span className="muted">Angefragt von: {ticket.requestedBy}</span>}
|
||||||
|
<span className="grow" />
|
||||||
|
<div style={{ minWidth: 150 }}><EditorDropdown value={ticket.assignedTo} onChange={(v) => onUpdate(ticket.$id, { assignedTo: v })} /></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs tabs={TABS} active={activeTab} onChange={setActiveTab} />
|
||||||
|
|
||||||
|
<div className="ticket-tab-content">
|
||||||
|
{activeTab === 'details' && (
|
||||||
|
<div>
|
||||||
|
<div className="ui-card pad" style={{ marginBottom: 16 }}>
|
||||||
|
<h5 style={{ fontWeight: 700, marginBottom: 12 }}>Ticket-Beschreibung</h5>
|
||||||
|
<p style={{ whiteSpace: 'pre-wrap', color: 'var(--text-muted)', lineHeight: 1.7, margin: 0 }}>
|
||||||
|
{ticket.details || 'Keine Details vorhanden.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ProjectContextPanel ticket={ticket} />
|
||||||
|
<TicketRoadmapPanel ticket={ticket} />
|
||||||
|
<button className="ui-btn ui-btn-primary" style={{ marginBottom: 12 }} onClick={() => setShowProjectsModal(true)}>
|
||||||
|
<FaPen /> Projekte zuweisen / bearbeiten
|
||||||
|
</button>
|
||||||
|
<TicketAssignedProjectsPanel ticket={ticket} refreshKey={projectsRefreshKey} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'worksheets' && (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-2" style={{ marginBottom: 16 }}>
|
||||||
|
<button className="ui-btn ui-btn-primary grow" onClick={() => setShowCreateWorksheet(true)}>
|
||||||
|
<FaPlus /> Arbeitsblatt hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<WorksheetSummary worksheets={worksheets} />
|
||||||
|
<WorksheetList
|
||||||
|
worksheets={worksheets}
|
||||||
|
totalTime={getTotalTime()}
|
||||||
|
loading={worksheetsLoading}
|
||||||
|
canEditTimes={(ws) => canEditWorksheetTimes(ws, user, employee)}
|
||||||
|
onEditTimes={setEditTimesWorksheet}
|
||||||
|
onTogglePlan={handleTogglePlan}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'invoices' && <InvoicePanel ticket={ticket} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CreateWorksheetModal isOpen={showCreateWorksheet} onClose={() => setShowCreateWorksheet(false)} workorder={ticket} onCreate={handleCreateWorksheet} />
|
||||||
|
{editTimesWorksheet && (
|
||||||
|
<WorksheetTimesModal
|
||||||
|
worksheet={editTimesWorksheet}
|
||||||
|
onClose={() => setEditTimesWorksheet(null)}
|
||||||
|
onSave={(ws, times) => updateWorksheetTimes(ws, times, user, employee)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<TicketProjectsModal isOpen={showProjectsModal} onClose={() => setShowProjectsModal(false)} ticket={ticket} onUpdated={() => setProjectsRefreshKey((k) => k + 1)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import { FaCaretDown } from 'react-icons/fa6'
|
|
||||||
|
|
||||||
const 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 = [
|
|
||||||
'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 STATUSES = [
|
|
||||||
'Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info',
|
|
||||||
'In Test', 'Halted', 'Aborted', 'Cancelled', 'Closed'
|
|
||||||
]
|
|
||||||
|
|
||||||
const PRIORITIES = [
|
|
||||||
{ value: 0, label: 'None' },
|
|
||||||
{ value: 1, label: 'Low' },
|
|
||||||
{ value: 2, label: 'Medium' },
|
|
||||||
{ value: 3, label: 'High' },
|
|
||||||
{ value: 4, label: 'Critical' }
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function TicketFilters({ filters, onChange, onApply }) {
|
|
||||||
const [localFilters, setLocalFilters] = useState(filters)
|
|
||||||
|
|
||||||
const handleCheckboxChange = (category, value) => {
|
|
||||||
const current = localFilters[category] || []
|
|
||||||
const updated = current.includes(value)
|
|
||||||
? current.filter(v => v !== value)
|
|
||||||
: [...current, value]
|
|
||||||
setLocalFilters({ ...localFilters, [category]: updated })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputChange = (field, value) => {
|
|
||||||
setLocalFilters({ ...localFilters, [field]: value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleApply = () => {
|
|
||||||
onChange(localFilters)
|
|
||||||
onApply()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className="bg-dark-grey text-white">
|
|
||||||
<td className="text-center">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="WOID"
|
|
||||||
className="form-control"
|
|
||||||
style={{ width: '80px', background: '#616161', color: '#fff', border: 'none' }}
|
|
||||||
value={localFilters.woid || ''}
|
|
||||||
onChange={(e) => handleInputChange('woid', e.target.value)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Created"
|
|
||||||
className="form-control"
|
|
||||||
style={{ width: '100px', background: '#616161', color: '#fff', border: 'none' }}
|
|
||||||
value={localFilters.createdDate || ''}
|
|
||||||
onChange={(e) => handleInputChange('createdDate', e.target.value)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="btn btn-dark">
|
|
||||||
Type / Location <FaCaretDown />
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-content" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
|
||||||
{TICKET_TYPES.map(type => (
|
|
||||||
<label key={type} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(localFilters.type || []).includes(type)}
|
|
||||||
onChange={() => handleCheckboxChange('type', type)}
|
|
||||||
/>
|
|
||||||
{type}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="btn btn-dark">
|
|
||||||
System <FaCaretDown />
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-content" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
|
||||||
{SYSTEMS.map(system => (
|
|
||||||
<label key={system} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(localFilters.system || []).includes(system)}
|
|
||||||
onChange={() => handleCheckboxChange('system', system)}
|
|
||||||
/>
|
|
||||||
{system}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Customer or Location"
|
|
||||||
className="form-control"
|
|
||||||
style={{ width: '150px', background: '#616161', color: '#fff', border: 'none' }}
|
|
||||||
value={localFilters.customer || ''}
|
|
||||||
onChange={(e) => handleInputChange('customer', e.target.value)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="User or Topic"
|
|
||||||
className="form-control"
|
|
||||||
style={{ width: '150px', background: '#616161', color: '#fff', border: 'none' }}
|
|
||||||
value={localFilters.userTopic || ''}
|
|
||||||
onChange={(e) => handleInputChange('userTopic', e.target.value)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="btn btn-dark">
|
|
||||||
Status <FaCaretDown />
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-content">
|
|
||||||
{STATUSES.map(status => (
|
|
||||||
<label key={status} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(localFilters.status || []).includes(status)}
|
|
||||||
onChange={() => handleCheckboxChange('status', status)}
|
|
||||||
/>
|
|
||||||
{status}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="btn btn-dark">
|
|
||||||
Priority <FaCaretDown />
|
|
||||||
</button>
|
|
||||||
<div className="dropdown-content">
|
|
||||||
{PRIORITIES.map(p => (
|
|
||||||
<label key={p.value} className="dropdown-item" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(localFilters.priority || []).includes(p.value)}
|
|
||||||
onChange={() => handleCheckboxChange('priority', p.value)}
|
|
||||||
/>
|
|
||||||
{p.label}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="text-center">Approval</td>
|
|
||||||
<td className="text-center">
|
|
||||||
<button className="btn btn-green" onClick={handleApply}>
|
|
||||||
Apply!
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { FaPlus, FaTimes } from 'react-icons/fa'
|
||||||
|
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
|
||||||
|
import { createProjectFromTemplate } from '../lib/projectAdminApi'
|
||||||
|
import AssignedProjectCard from './AssignedProjectCard'
|
||||||
|
|
||||||
|
function slugify(v) {
|
||||||
|
return String(v || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TicketProjectsModal({ isOpen, onClose, ticket, onUpdated }) {
|
||||||
|
const { fetchAllProjects, fetchByTicketId, assignProjects, unassignProject } = useWebsiteProjects()
|
||||||
|
const [assigned, setAssigned] = useState([])
|
||||||
|
const [available, setAvailable] = useState([])
|
||||||
|
const [selectedIds, setSelectedIds] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [actionLoading, setActionLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [createForm, setCreateForm] = useState({ displayName: '', subdomain: '', repoName: '' })
|
||||||
|
|
||||||
|
const loadProjects = useCallback(async () => {
|
||||||
|
if (!ticket?.$id) return
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const [all, mine] = await Promise.all([fetchAllProjects(), fetchByTicketId(ticket.$id)])
|
||||||
|
setAssigned(mine)
|
||||||
|
const assignedIds = new Set(mine.map((p) => p.$id))
|
||||||
|
setAvailable((all || []).filter((p) => p.repoFullName && !assignedIds.has(p.$id)))
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [ticket, fetchAllProjects, fetchByTicketId])
|
||||||
|
|
||||||
|
useEffect(() => { if (isOpen) loadProjects() }, [isOpen, loadProjects])
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
const handleAssign = async () => {
|
||||||
|
if (!selectedIds.length) return
|
||||||
|
setActionLoading(true)
|
||||||
|
const r = await assignProjects(selectedIds, { customerId: ticket.customerId || '', ticketId: ticket.$id })
|
||||||
|
setActionLoading(false)
|
||||||
|
if (r.success) { setSelectedIds([]); await loadProjects(); onUpdated?.() } else setError(r.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreate = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setActionLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
await createProjectFromTemplate({
|
||||||
|
repoName: createForm.repoName || createForm.subdomain,
|
||||||
|
displayName: createForm.displayName,
|
||||||
|
subdomain: slugify(createForm.subdomain),
|
||||||
|
customerId: ticket.customerId || '',
|
||||||
|
ticketId: ticket.$id,
|
||||||
|
})
|
||||||
|
setCreateForm({ displayName: '', subdomain: '', repoName: '' })
|
||||||
|
await loadProjects()
|
||||||
|
onUpdated?.()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message)
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUnassign = async (projectId) => {
|
||||||
|
setActionLoading(true)
|
||||||
|
await unassignProject(projectId)
|
||||||
|
setActionLoading(false)
|
||||||
|
await loadProjects()
|
||||||
|
onUpdated?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overlay ticket-projects-overlay">
|
||||||
|
<span className="overlay-close" onClick={onClose}>{'\u00d7'}</span>
|
||||||
|
<div className="overlay-content ticket-projects-modal">
|
||||||
|
<div className="ticket-projects-modal-header">
|
||||||
|
<h2>Projekte bearbeiten</h2>
|
||||||
|
<button type="button" className="btn btn-sm" onClick={onClose} aria-label="Schliessen"><FaTimes /></button>
|
||||||
|
</div>
|
||||||
|
{error && <div className="bg-red text-white p-2 mb-2">{error}</div>}
|
||||||
|
{loading ? <p className="text-grey">Projekte werden geladen...</p> : (
|
||||||
|
<div className="ticket-projects-modal-grid">
|
||||||
|
<section className="ticket-projects-modal-section">
|
||||||
|
<h6>Zugewiesen</h6>
|
||||||
|
{assigned.length === 0 ? <p className="text-grey">Keine Projekte zugewiesen.</p> : assigned.map((p) => (
|
||||||
|
<AssignedProjectCard key={p.$id} project={p} showActions onUnassign={handleUnassign} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
<div className="ticket-projects-modal-side">
|
||||||
|
<section className="ticket-projects-modal-section">
|
||||||
|
<h6>Verfuegbar zuweisen</h6>
|
||||||
|
{available.length === 0 ? <p className="text-grey">Keine weiteren Projekte verfuegbar.</p> : (
|
||||||
|
<div className="ticket-projects-available-list">
|
||||||
|
{available.map((p) => (
|
||||||
|
<label key={p.$id} className="ticket-projects-available-item">
|
||||||
|
<input type="checkbox" checked={selectedIds.includes(p.$id)} onChange={() => setSelectedIds((s) => s.includes(p.$id) ? s.filter((x) => x !== p.$id) : [...s, p.$id])} />
|
||||||
|
<span>{p.projectName} ({p.subdomain || p.repoFullName})</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button type="button" className="btn btn-teal mt-2" disabled={!selectedIds.length || actionLoading} onClick={handleAssign}>Zuweisen</button>
|
||||||
|
</section>
|
||||||
|
<section className="ticket-projects-modal-section">
|
||||||
|
<h6><FaPlus /> Neues Projekt</h6>
|
||||||
|
<form onSubmit={handleCreate} className="ticket-projects-create-form">
|
||||||
|
<input className="form-control" placeholder="Anzeigename" value={createForm.displayName} onChange={(e) => setCreateForm((f) => ({ ...f, displayName: e.target.value }))} required />
|
||||||
|
<input className="form-control" placeholder="Subdomain" value={createForm.subdomain} onChange={(e) => setCreateForm((f) => ({ ...f, subdomain: e.target.value, repoName: f.repoName || slugify(e.target.value) }))} required />
|
||||||
|
<button type="submit" className="btn btn-dark" disabled={actionLoading}>{actionLoading ? 'Wird angelegt...' : 'Anlegen & zuweisen'}</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { IconRoute, IconAlertTriangle } from '@tabler/icons-react'
|
||||||
|
import RoadmapProgressBar from './roadmap/RoadmapProgressBar'
|
||||||
|
import {
|
||||||
|
fetchRoadmapByTicketId,
|
||||||
|
fetchRoadmapNodes,
|
||||||
|
computeProgress,
|
||||||
|
isNodeOverdue,
|
||||||
|
NODE_KIND,
|
||||||
|
NODE_STATUS,
|
||||||
|
ROADMAP_STATUS,
|
||||||
|
} from '../lib/roadmaps'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roadmap-Tracking im Projektticket: Fortschritt, Mini-Uebersicht und Link zum Board.
|
||||||
|
* Rendert nichts, wenn zum Ticket keine Roadmap existiert.
|
||||||
|
*/
|
||||||
|
export default function TicketRoadmapPanel({ ticket }) {
|
||||||
|
const [roadmap, setRoadmap] = useState(null)
|
||||||
|
const [nodes, setNodes] = useState([])
|
||||||
|
const [loaded, setLoaded] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const doc = await fetchRoadmapByTicketId(ticket.$id)
|
||||||
|
if (cancelled) return
|
||||||
|
setRoadmap(doc)
|
||||||
|
if (doc) setNodes(await fetchRoadmapNodes(doc.$id))
|
||||||
|
} catch {
|
||||||
|
/* Panel ist optional */
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoaded(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [ticket.$id])
|
||||||
|
|
||||||
|
if (!loaded || !roadmap) return null
|
||||||
|
|
||||||
|
const progress = computeProgress(nodes)
|
||||||
|
const relevant = nodes.filter((n) => n.kind !== NODE_KIND.START)
|
||||||
|
const counts = {
|
||||||
|
open: relevant.filter((n) => (n.status || NODE_STATUS.OPEN) === NODE_STATUS.OPEN).length,
|
||||||
|
inProgress: relevant.filter((n) => n.status === NODE_STATUS.IN_PROGRESS).length,
|
||||||
|
done: relevant.filter((n) => n.status === NODE_STATUS.DONE).length,
|
||||||
|
}
|
||||||
|
const nextDue = relevant
|
||||||
|
.filter((n) => n.deadline && n.status !== NODE_STATUS.DONE)
|
||||||
|
.sort((a, b) => a.deadline.localeCompare(b.deadline))
|
||||||
|
.slice(0, 3)
|
||||||
|
const closedButActive = ticket.status === 'Closed' && roadmap.status === ROADMAP_STATUS.ACTIVE
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ui-card pad" style={{ marginBottom: 16 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap', marginBottom: 10 }}>
|
||||||
|
<h5 style={{ fontWeight: 700, margin: 0, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<IconRoute size={18} /> Roadmap: {roadmap.title}
|
||||||
|
</h5>
|
||||||
|
<Link to={`/roadmaps/${roadmap.$id}`} className="ui-btn">Zur Roadmap</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RoadmapProgressBar progress={progress} />
|
||||||
|
|
||||||
|
<div className="muted" style={{ display: 'flex', gap: 14, fontSize: 13, marginTop: 10, flexWrap: 'wrap' }}>
|
||||||
|
<span>Offen: {counts.open}</span>
|
||||||
|
<span>In Arbeit: {counts.inProgress}</span>
|
||||||
|
<span>Fertig: {counts.done}</span>
|
||||||
|
{roadmap.status === ROADMAP_STATUS.COMPLETED && <span style={{ color: '#34d399' }}>Plan abgeschlossen</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{nextDue.length > 0 && (
|
||||||
|
<div style={{ marginTop: 10, fontSize: 13 }}>
|
||||||
|
<span className="faint">Naechste Deadlines: </span>
|
||||||
|
{nextDue.map((n, i) => (
|
||||||
|
<span key={n.$id} style={isNodeOverdue(n) ? { color: 'var(--danger, #ef4444)', fontWeight: 700 } : undefined}>
|
||||||
|
{i > 0 && ' · '}{n.title} ({n.deadline.split('-').reverse().join('.')})
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{closedButActive && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, fontSize: 13, color: '#fbbf24' }}>
|
||||||
|
<IconAlertTriangle size={16} style={{ flexShrink: 0 }} />
|
||||||
|
Ticket ist geschlossen, aber der Plan ist erst zu {progress.percent}% fertig.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
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',
|
|
||||||
1: 'priority-low',
|
|
||||||
2: 'priority-medium',
|
|
||||||
3: 'priority-high',
|
|
||||||
4: 'priority-critical'
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRIORITY_LABELS = {
|
|
||||||
0: 'NONE',
|
|
||||||
1: 'LOW',
|
|
||||||
2: 'MEDIUM',
|
|
||||||
3: 'HIGH',
|
|
||||||
4: 'CRITICAL'
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_CLASSES = {
|
|
||||||
'Open': 'status-open',
|
|
||||||
'Occupied': 'status-occupied',
|
|
||||||
'Assigned': 'status-assigned',
|
|
||||||
'Awaiting': 'status-awaiting',
|
|
||||||
'Closed': 'status-closed'
|
|
||||||
}
|
|
||||||
|
|
||||||
const APPROVAL_ICONS = {
|
|
||||||
'pending': FaUserGear,
|
|
||||||
'approved': FaSackDollar,
|
|
||||||
'quote': FaSackDollar,
|
|
||||||
'shipping': FaTruck
|
|
||||||
}
|
|
||||||
|
|
||||||
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 })
|
|
||||||
|
|
||||||
const handleStatusChange = (newStatus) => {
|
|
||||||
onUpdate(ticket.$id, { status: newStatus })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePriorityChange = (newPriority) => {
|
|
||||||
onUpdate(ticket.$id, { priority: newPriority })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEditorChange = (newEditor) => {
|
|
||||||
onUpdate(ticket.$id, { assignedTo: newEditor })
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResponseChange = (newResponse) => {
|
|
||||||
onUpdate(ticket.$id, { responseLevel: newResponse })
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleLock = () => {
|
|
||||||
if (locked) {
|
|
||||||
setExpanded(true)
|
|
||||||
onExpand?.(ticket.$id)
|
|
||||||
} else {
|
|
||||||
setExpanded(false)
|
|
||||||
}
|
|
||||||
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 ${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}>
|
|
||||||
<strong>{format(createdAt, 'dd.MM.yyyy')}</strong>
|
|
||||||
</td>
|
|
||||||
<td className="ticket-info" rowSpan={2}>
|
|
||||||
<strong>{ticket.type}</strong>
|
|
||||||
<br />
|
|
||||||
{ticket.serviceType || 'Remote'}
|
|
||||||
<br />
|
|
||||||
<FaPlay className="text-green" /> {ticket.startDate || format(createdAt, 'dd.MM.yyyy')}
|
|
||||||
<br />
|
|
||||||
<FaStop className="text-red" /> {ticket.deadline || '-'}
|
|
||||||
</td>
|
|
||||||
<td className="ticket-info" rowSpan={2}>
|
|
||||||
<strong>{ticket.systemType || 'n/a'}</strong>
|
|
||||||
</td>
|
|
||||||
<td rowSpan={2}>
|
|
||||||
<strong>{ticket.customerName || 'Unknown'}</strong>
|
|
||||||
<br />
|
|
||||||
{ticket.customerLocation || ''}
|
|
||||||
</td>
|
|
||||||
<td rowSpan={2}>
|
|
||||||
<strong>{ticket.requestedBy || '-'}</strong>
|
|
||||||
<br />
|
|
||||||
{ticket.requestedFor && <>Requested for: {ticket.requestedFor}<br /></>}
|
|
||||||
{ticket.topic}
|
|
||||||
</td>
|
|
||||||
<td className={`text-center ${STATUS_CLASSES[ticket.status] || 'status-open'}`}>
|
|
||||||
<StatusDropdown
|
|
||||||
value={ticket.status}
|
|
||||||
onChange={handleStatusChange}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className={`text-center ${PRIORITY_CLASSES[ticket.priority] || 'priority-low'}`}>
|
|
||||||
<PriorityDropdown
|
|
||||||
value={ticket.priority}
|
|
||||||
onChange={handlePriorityChange}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={`text-center ${ticket.approvalStatus === 'approved' ? 'bg-green' : 'bg-yellow'}`}
|
|
||||||
rowSpan={2}
|
|
||||||
style={{ verticalAlign: 'middle' }}
|
|
||||||
>
|
|
||||||
<ApprovalIcon size={24} />
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="bg-dark-grey text-center text-white"
|
|
||||||
rowSpan={2}
|
|
||||||
style={{ verticalAlign: 'middle', cursor: 'pointer' }}
|
|
||||||
onClick={toggleLock}
|
|
||||||
>
|
|
||||||
{locked ? <FaLock size={24} /> : <FaLockOpen size={24} />}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr className="ticket-row">
|
|
||||||
<td className={`text-center ${STATUS_CLASSES[ticket.status] || 'status-open'}`}>
|
|
||||||
<EditorDropdown
|
|
||||||
value={ticket.assignedTo}
|
|
||||||
onChange={handleEditorChange}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className={`text-center ${PRIORITY_CLASSES[ticket.priority] || 'priority-low'}`}>
|
|
||||||
<ResponseDropdown
|
|
||||||
value={ticket.responseLevel}
|
|
||||||
onChange={handleResponseChange}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{expanded && (
|
|
||||||
<>
|
|
||||||
<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: '12px', background: 'transparent', border: 'none' }}></td>
|
|
||||||
</tr>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { isUrgent, priorityFor } from '../lib/ticketStatus'
|
||||||
|
|
||||||
|
// Ein Klick markiert ein Ticket als dringend oder wieder als normal
|
||||||
|
export default function UrgentToggle({ value, onChange }) {
|
||||||
|
const urgent = isUrgent(value)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`urgent-toggle${urgent ? ' on' : ''}`}
|
||||||
|
aria-pressed={urgent}
|
||||||
|
title={urgent ? 'Nicht mehr dringend' : 'Als dringend markieren'}
|
||||||
|
onClick={() => onChange(priorityFor(!urgent))}
|
||||||
|
>
|
||||||
|
Dringend
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +1,36 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp } from 'react-icons/fa'
|
import { FaClock, FaUser, FaExchangeAlt, FaComment, FaChevronDown, FaChevronUp, FaPen, FaCheck } from 'react-icons/fa'
|
||||||
|
import { serviceTypeLabel, needsTimeEntry, worksheetTone } from '../lib/ticketForm'
|
||||||
|
import { statusLabel } from '../lib/ticketStatus'
|
||||||
|
|
||||||
export default function WorksheetList({ worksheets, totalTime, loading }) {
|
// Kopffarbe je Zustand (worksheetTone): orange = Git-Zeit fehlt, gruen = Git-Zeit eingetragen
|
||||||
|
const TONES = {
|
||||||
|
'git-open': { border: '#f97316', base: 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)', hover: 'linear-gradient(135deg, #ea580c 0%, #c2410c 100%)' },
|
||||||
|
'git-done': { border: '#10b981', base: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', hover: 'linear-gradient(135deg, #059669 0%, #047857 100%)' },
|
||||||
|
comment: { border: '#3b82f6', base: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', hover: 'linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%)' },
|
||||||
|
work: { border: '#4a5568', base: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)', hover: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hinweise im farbigen Kopf: mit der grauen Badge-Schrift waeren sie kaum lesbar
|
||||||
|
const headerBadgeStyle = { background: 'rgba(255,255,255,0.3)', color: 'white', marginLeft: '8px' }
|
||||||
|
|
||||||
|
const headerButtonStyle = {
|
||||||
|
background: 'rgba(255,255,255,0.25)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.4)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '2px 10px',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
marginRight: '8px',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WorksheetList({ worksheets, totalTime, loading, onTogglePlan, canEditTimes, onEditTimes }) {
|
||||||
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
const [expandedWorksheets, setExpandedWorksheets] = useState({})
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center p-4">
|
<div className="text-center p-4">
|
||||||
<div className="spinner-border" role="status">
|
<div className="spinner-border" role="status">
|
||||||
<span className="visually-hidden">Loading...</span>
|
<span className="visually-hidden">Lädt …</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -17,7 +40,7 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
return (
|
return (
|
||||||
<div className="alert alert-info" role="alert">
|
<div className="alert alert-info" role="alert">
|
||||||
<FaComment className="me-2" />
|
<FaComment className="me-2" />
|
||||||
Noch keine Worksheets vorhanden. Erstelle das erste Worksheet für dieses Ticket.
|
Noch keine Arbeitsblätter. Leg das erste mit „Arbeitsblatt hinzufügen“ an.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -49,13 +72,16 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
<div className="timeline">
|
<div className="timeline">
|
||||||
{worksheets.map((ws, index) => {
|
{worksheets.map((ws, index) => {
|
||||||
const isExpanded = expandedWorksheets[ws.wsid] || false
|
const isExpanded = expandedWorksheets[ws.wsid] || false
|
||||||
|
const isGit = ws.serviceType === 'GIT'
|
||||||
|
const tone = worksheetTone(ws)
|
||||||
|
const colors = TONES[tone]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={ws.$id} className="timeline-item mb-4" style={{
|
<div key={ws.$id} className="timeline-item mb-4" style={{
|
||||||
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
|
animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s backwards`
|
||||||
}}>
|
}}>
|
||||||
<div className="card border-0 shadow-sm overflow-hidden" style={{
|
<div className="card border-0 shadow-sm overflow-hidden" style={{
|
||||||
borderLeft: ws.isComment ? '4px solid #10b981' : '4px solid #4a5568',
|
borderLeft: `4px solid ${colors.border}`,
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
|
||||||
}} onMouseEnter={(e) => {
|
}} onMouseEnter={(e) => {
|
||||||
@@ -70,9 +96,7 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
className="card-header d-flex justify-content-between align-items-center py-3"
|
className="card-header d-flex justify-content-between align-items-center py-3"
|
||||||
onClick={() => toggleWorksheet(ws.wsid)}
|
onClick={() => toggleWorksheet(ws.wsid)}
|
||||||
style={{
|
style={{
|
||||||
background: ws.isComment
|
background: colors.base,
|
||||||
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
|
||||||
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -80,14 +104,10 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
transition: 'background 0.2s ease'
|
transition: 'background 0.2s ease'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = ws.isComment
|
e.currentTarget.style.background = colors.hover
|
||||||
? 'linear-gradient(135deg, #059669 0%, #047857 100%)'
|
|
||||||
: 'linear-gradient(135deg, #2d3748 0%, #1a202c 100%)'
|
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = ws.isComment
|
e.currentTarget.style.background = colors.base
|
||||||
? 'linear-gradient(135deg, #10b981 0%, #059669 100%)'
|
|
||||||
: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="d-flex align-items-center gap-3">
|
<div className="d-flex align-items-center gap-3">
|
||||||
@@ -98,13 +118,26 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<strong className="fs-6">WSID {ws.wsid}</strong>
|
<strong className="fs-6">WSID {ws.wsid}</strong>
|
||||||
{ws.isComment && (
|
{ws.isComment && !isGit && (
|
||||||
<span className="badge ms-2" style={{
|
<span className="badge ms-2" style={headerBadgeStyle}>
|
||||||
background: 'rgba(255,255,255,0.3)'
|
|
||||||
}}>
|
|
||||||
<FaComment className="me-1" /> Kommentar
|
<FaComment className="me-1" /> Kommentar
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{tone === 'git-open' && (
|
||||||
|
<span className="badge ms-2" style={headerBadgeStyle}>
|
||||||
|
<FaClock className="me-1" /> Zeit offen
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{tone === 'git-done' && (
|
||||||
|
<span className="badge ms-2" style={headerBadgeStyle}>
|
||||||
|
<FaCheck className="me-1" /> Zeit eingetragen
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ws.countsToPlan && !ws.isComment && (
|
||||||
|
<span className="badge ms-2" title="Wird vom Stundenguthaben des Wartungs-Abos abgezogen" style={{ ...headerBadgeStyle, background: '#9d3dfe' }}>
|
||||||
|
Abo-Stunden
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Collapsed: Mitarbeiter & Zeit im Header */}
|
{/* Collapsed: Mitarbeiter & Zeit im Header */}
|
||||||
{!isExpanded && (
|
{!isExpanded && (
|
||||||
@@ -119,16 +152,57 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
<span style={{ fontSize: '0.9rem' }}>{formatTime(ws.totalTime)}</span>
|
<span style={{ fontSize: '0.9rem' }}>{formatTime(ws.totalTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="badge" style={{
|
<span className="badge" style={{
|
||||||
background: 'rgba(255,255,255,0.2)',
|
background: 'rgba(255,255,255,0.2)',
|
||||||
|
color: 'white',
|
||||||
fontSize: '0.8rem'
|
fontSize: '0.8rem'
|
||||||
}}>{ws.serviceType}</span>
|
}}>{serviceTypeLabel(ws.serviceType)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<small style={{ opacity: 0.9 }}>
|
<div className="d-flex align-items-center gap-2">
|
||||||
{formatDateTime(ws.startDate, ws.startTime)}
|
{onEditTimes && canEditTimes?.(ws) && (
|
||||||
</small>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm"
|
||||||
|
title="Beginn, Ende und Arbeitszeit ändern"
|
||||||
|
style={headerButtonStyle}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onEditTimes(ws)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{needsTimeEntry(ws)
|
||||||
|
? <><FaClock className="me-1" /> Zeit nachtragen</>
|
||||||
|
: <><FaPen className="me-1" /> Zeiten bearbeiten</>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onTogglePlan && !ws.isComment && Number(ws.totalTime) > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm"
|
||||||
|
title={ws.countsToPlan ? 'Nicht mehr vom Abo-Stundenkonto abziehen' : 'Vom Abo-Stundenkonto des Kunden abziehen'}
|
||||||
|
style={{
|
||||||
|
background: ws.countsToPlan ? '#9d3dfe' : 'rgba(255,255,255,0.15)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255,255,255,0.4)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '2px 10px',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
marginRight: '8px',
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onTogglePlan(ws)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ws.countsToPlan ? '✓ Abo' : '+ Abo'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<small style={{ opacity: 0.9 }}>
|
||||||
|
{formatDateTime(ws.startDate, ws.startTime) || formatDateTime(ws.endDate, ws.endTime)}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body - Nur wenn expanded */}
|
{/* Body - Nur wenn expanded */}
|
||||||
@@ -169,7 +243,7 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
background: 'linear-gradient(135deg, #4a5568 0%, #2d3748 100%)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontSize: '0.85rem'
|
fontSize: '0.85rem'
|
||||||
}}>{ws.serviceType}</span>
|
}}>{serviceTypeLabel(ws.serviceType)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status-Änderung */}
|
{/* Status-Änderung */}
|
||||||
@@ -179,19 +253,9 @@ export default function WorksheetList({ worksheets, totalTime, loading }) {
|
|||||||
}}>
|
}}>
|
||||||
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
|
<FaExchangeAlt className="me-2" style={{ color: '#10b981' }} />
|
||||||
<span className="text-muted">Status:</span>{' '}
|
<span className="text-muted">Status:</span>{' '}
|
||||||
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{ws.oldStatus}</span>
|
<span className="badge" style={{ background: '#6b7280', color: 'white' }}>{statusLabel(ws.oldStatus)}</span>
|
||||||
<span className="mx-2" style={{ color: '#10b981' }}>→</span>
|
<span className="mx-2" style={{ color: '#10b981' }}>→</span>
|
||||||
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{ws.newStatus}</span>
|
<span className="badge" style={{ background: '#10b981', color: 'white' }}>{statusLabel(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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { needsTimeEntry } from '../lib/ticketForm'
|
||||||
|
|
||||||
|
const formatTime = (minutes) => {
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
const mins = minutes % 60
|
||||||
|
return hours > 0 ? `${hours}h ${mins}min` : `${mins}min`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eine Zeile ueber der Arbeitsblatt-Liste: Arbeitszeit, Anzahl, offene Git-Zeiten
|
||||||
|
export default function WorksheetSummary({ worksheets }) {
|
||||||
|
if (!worksheets || worksheets.length === 0) return null
|
||||||
|
const minutes = worksheets
|
||||||
|
.filter((ws) => !ws.isComment)
|
||||||
|
.reduce((sum, ws) => sum + (Number(ws.totalTime) || 0), 0)
|
||||||
|
const open = worksheets.filter(needsTimeEntry).length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="worksheet-summary">
|
||||||
|
<span>Arbeitszeit gesamt <strong>{formatTime(minutes)}</strong></span>
|
||||||
|
<span>{worksheets.length} {worksheets.length === 1 ? 'Arbeitsblatt' : 'Arbeitsblätter'}</span>
|
||||||
|
{open > 0 && <span className="open">{open} {open === 1 ? 'Zeit offen' : 'Zeiten offen'}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
|
import { isoToGermanDate, timeToWoms, germanDateToIso, womsTimeToInput, minutesBetween, isGitWorksheet, needsTimeEntry } from '../lib/ticketForm'
|
||||||
|
import { DateTimeInput } from './ui'
|
||||||
|
|
||||||
|
// Gespeichert ist dd.mm.yyyy / hhmm, die Kalender-Felder brauchen yyyy-mm-dd / hh:mm
|
||||||
|
const formFromWorksheet = (worksheet) => ({
|
||||||
|
startDate: germanDateToIso(worksheet.startDate || worksheet.endDate),
|
||||||
|
startTime: womsTimeToInput(worksheet.startTime),
|
||||||
|
endDate: germanDateToIso(worksheet.endDate || worksheet.startDate),
|
||||||
|
endTime: womsTimeToInput(worksheet.endTime)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beginn, Ende und Arbeitszeit eines Arbeitsblatts nachtraeglich aendern.
|
||||||
|
* Wer das darf, entscheidet canEditWorksheetTimes (lib/ticketForm).
|
||||||
|
*/
|
||||||
|
export default function WorksheetTimesModal({ worksheet, onClose, onSave }) {
|
||||||
|
const isGit = isGitWorksheet(worksheet)
|
||||||
|
const [formData, setFormData] = useState(() => formFromWorksheet(worksheet))
|
||||||
|
// null = die Arbeitszeit folgt Beginn und Ende. Passte sie schon vorher nicht dazu,
|
||||||
|
// wurde sie von Hand eingetragen (z.B. ohne Pause) und bleibt stehen.
|
||||||
|
const [manualTotal, setManualTotal] = useState(() => {
|
||||||
|
const stored = Number(worksheet.totalTime) || 0
|
||||||
|
return !stored || stored === minutesBetween(formData.startTime, formData.endTime) ? null : stored
|
||||||
|
})
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const calculatedTotal = minutesBetween(formData.startTime, formData.endTime)
|
||||||
|
const totalTime = manualTotal ?? calculatedTotal ?? 0
|
||||||
|
|
||||||
|
const handleChange = (field, value) => {
|
||||||
|
setFormData(prev => ({ ...prev, [field]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Das Ende darf nicht vor dem Start liegen, deshalb rueckt es mit
|
||||||
|
const handleStartDateChange = (value) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
startDate: value,
|
||||||
|
endDate: value && prev.endDate && prev.endDate < value ? value : prev.endDate
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setSaving(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const result = await onSave(worksheet, {
|
||||||
|
startDate: isoToGermanDate(formData.startDate),
|
||||||
|
startTime: timeToWoms(formData.startTime),
|
||||||
|
endDate: isoToGermanDate(formData.endDate),
|
||||||
|
endTime: timeToWoms(formData.endTime),
|
||||||
|
totalTime: parseInt(totalTime, 10) || 0
|
||||||
|
})
|
||||||
|
if (result?.success) {
|
||||||
|
onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError(result?.error || 'Speichern fehlgeschlagen')
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Speichern fehlgeschlagen')
|
||||||
|
}
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overlay">
|
||||||
|
<span className="overlay-close" onClick={onClose}>
|
||||||
|
<FaTimes />
|
||||||
|
</span>
|
||||||
|
<div className="overlay-content" style={{ maxWidth: 560 }}>
|
||||||
|
{/* Kurzer Titel: auf dem Handy laeuft ein langer unter das Schliessen-X */}
|
||||||
|
<h2 className="mb-2">{needsTimeEntry(worksheet) ? 'Zeit nachtragen' : 'Zeiten bearbeiten'}</h2>
|
||||||
|
<p className="text-grey" style={{ fontSize: '14px', marginBottom: '16px' }}>
|
||||||
|
WSID {worksheet.wsid}
|
||||||
|
{isGit && <><br />Das Ende ist der Zeitpunkt des Git-Pushs. Trag ein, wann die Arbeit begonnen hat.</>}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red text-white p-2 mb-2" style={{ borderRadius: '4px' }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="worksheet-times-start">Beginn</label>
|
||||||
|
<DateTimeInput
|
||||||
|
id="worksheet-times-start"
|
||||||
|
timeLabel="Uhrzeit Beginn"
|
||||||
|
required
|
||||||
|
timeRequired={isGit}
|
||||||
|
date={formData.startDate}
|
||||||
|
time={formData.startTime}
|
||||||
|
onDateChange={handleStartDateChange}
|
||||||
|
onTimeChange={(value) => handleChange('startTime', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="worksheet-times-end">Ende</label>
|
||||||
|
<DateTimeInput
|
||||||
|
id="worksheet-times-end"
|
||||||
|
timeLabel="Uhrzeit Ende"
|
||||||
|
required
|
||||||
|
min={formData.startDate}
|
||||||
|
date={formData.endDate}
|
||||||
|
time={formData.endTime}
|
||||||
|
onDateChange={(value) => handleChange('endDate', value)}
|
||||||
|
onTimeChange={(value) => handleChange('endTime', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label className="form-label" htmlFor="worksheet-times-total">Arbeitszeit (Minuten)</label>
|
||||||
|
<input
|
||||||
|
id="worksheet-times-total"
|
||||||
|
type="number"
|
||||||
|
className="form-control"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
value={totalTime}
|
||||||
|
onChange={(e) => setManualTotal(e.target.value)}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<small style={{ color: '#a0aec0', fontSize: '12px' }}>
|
||||||
|
{manualTotal === null && calculatedTotal !== null
|
||||||
|
? '✓ Automatisch berechnet'
|
||||||
|
: 'Manuell eingeben'}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-2">
|
||||||
|
<button type="submit" className="btn btn-dark" disabled={saving}>
|
||||||
|
{saving ? 'Speichert...' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { IconTrash, IconX } from '@tabler/icons-react'
|
||||||
|
import { Card, Button, Field } from '../ui'
|
||||||
|
import { useEmployees } from '../../hooks/useEmployees'
|
||||||
|
import { NODE_KIND, NODE_STATUS, NODE_STATUS_LABELS } from '../../lib/roadmaps'
|
||||||
|
|
||||||
|
const STATUS_ORDER = [NODE_STATUS.OPEN, NODE_STATUS.IN_PROGRESS, NODE_STATUS.DONE]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seitliches Detail-Panel fuer den ausgewaehlten Knoten:
|
||||||
|
* Titel, Status, Zustaendiger, Deadline, Notizen, eingehende Verbindungen.
|
||||||
|
*/
|
||||||
|
export default function NodeDetailPanel({
|
||||||
|
node,
|
||||||
|
nodes,
|
||||||
|
readOnly,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
onSetStatus,
|
||||||
|
onDelete,
|
||||||
|
onDisconnect,
|
||||||
|
}) {
|
||||||
|
const { employees } = useEmployees()
|
||||||
|
const [form, setForm] = useState({ title: '', assignedTo: '', deadline: '', notes: '' })
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setForm({
|
||||||
|
title: node.title || '',
|
||||||
|
assignedTo: node.assignedTo || '',
|
||||||
|
deadline: node.deadline || '',
|
||||||
|
notes: node.notes || '',
|
||||||
|
})
|
||||||
|
setError('')
|
||||||
|
}, [node.$id]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const isStart = node.kind === NODE_KIND.START
|
||||||
|
const incoming = (node.sourceIds || [])
|
||||||
|
.map((id) => nodes.find((n) => n.$id === id))
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!form.title.trim()) {
|
||||||
|
setError('Titel darf nicht leer sein.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSaving(true)
|
||||||
|
setError('')
|
||||||
|
const employee = employees.find((e) => e.userId === form.assignedTo)
|
||||||
|
const result = await onSave(node, {
|
||||||
|
title: form.title.trim(),
|
||||||
|
assignedTo: form.assignedTo,
|
||||||
|
assignedName: employee?.displayName || '',
|
||||||
|
deadline: form.deadline,
|
||||||
|
notes: form.notes,
|
||||||
|
})
|
||||||
|
setSaving(false)
|
||||||
|
if (!result.success) setError(result.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={isStart ? 'Start-Knoten' : node.kind === NODE_KIND.GOAL ? 'Ziel-Knoten' : 'Schritt'}
|
||||||
|
actions={<Button variant="ghost" onClick={onClose}><IconX size={16} /></Button>}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
|
<Field label="Titel">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.title}
|
||||||
|
maxLength={128}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{!isStart && (
|
||||||
|
<Field label="Status">
|
||||||
|
<div style={{ display: 'flex', gap: 6 }}>
|
||||||
|
{STATUS_ORDER.map((status) => (
|
||||||
|
<Button
|
||||||
|
key={status}
|
||||||
|
variant={node.status === status ? 'primary' : 'default'}
|
||||||
|
disabled={readOnly}
|
||||||
|
onClick={() => onSetStatus(node, status)}
|
||||||
|
>
|
||||||
|
{NODE_STATUS_LABELS[status]}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isStart && (
|
||||||
|
<Field label="Zustaendig">
|
||||||
|
<select
|
||||||
|
value={form.assignedTo}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, assignedTo: e.target.value }))}
|
||||||
|
>
|
||||||
|
<option value="">Niemand</option>
|
||||||
|
{employees.map((e) => (
|
||||||
|
<option key={e.userId} value={e.userId}>{e.displayName}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isStart && (
|
||||||
|
<Field label="Deadline">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={form.deadline}
|
||||||
|
disabled={readOnly}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, deadline: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Field label="Notizen">
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={form.notes}
|
||||||
|
maxLength={2000}
|
||||||
|
disabled={readOnly}
|
||||||
|
placeholder="Details, Anforderungen, Links…"
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{incoming.length > 0 && (
|
||||||
|
<Field label="Eingehende Verbindungen">
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
{incoming.map((source) => (
|
||||||
|
<div key={source.$id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 13 }}>
|
||||||
|
<span className="muted">{source.title}</span>
|
||||||
|
{!readOnly && (
|
||||||
|
<Button variant="ghost" title="Verbindung loesen" onClick={() => onDisconnect(source.$id, node)}>
|
||||||
|
<IconX size={14} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && <p style={{ color: 'var(--danger, #ef4444)', fontSize: 13 }}>{error}</p>}
|
||||||
|
|
||||||
|
{!readOnly && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8 }}>
|
||||||
|
{!isStart ? (
|
||||||
|
<Button variant="ghost" onClick={() => onDelete(node)} title="Knoten loeschen">
|
||||||
|
<IconTrash size={15} /> Loeschen
|
||||||
|
</Button>
|
||||||
|
) : <span />}
|
||||||
|
<Button variant="primary" onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? 'Speichere…' : 'Speichern'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { useLayoutEffect, useRef, useState, useCallback } from 'react'
|
||||||
|
import { IconPlus } from '@tabler/icons-react'
|
||||||
|
import RoadmapNodeCard from './RoadmapNodeCard'
|
||||||
|
import { NODE_KIND } from '../../lib/roadmaps'
|
||||||
|
|
||||||
|
const COLUMN_WIDTH = 230
|
||||||
|
const COLUMN_GAP = 48
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Steckbrett: Spalten von links (Start) nach rechts (Ziele), Knoten vertikal
|
||||||
|
* gestapelt, Kanten als SVG-Overlay. Verbinden per Klick-Modus:
|
||||||
|
* Connect-Handle am Quellknoten -> gueltige Ziele (Spalte weiter rechts) anklicken.
|
||||||
|
*/
|
||||||
|
export default function RoadmapBoard({
|
||||||
|
roadmap,
|
||||||
|
nodes,
|
||||||
|
selectedNode,
|
||||||
|
readOnly,
|
||||||
|
onSelectNode,
|
||||||
|
onAddNode,
|
||||||
|
onConnect,
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
const nodeRefs = useRef({})
|
||||||
|
const [edges, setEdges] = useState([])
|
||||||
|
const [connectSource, setConnectSource] = useState(null)
|
||||||
|
|
||||||
|
const columnCount = Math.max(roadmap.columnCount || 3, 2)
|
||||||
|
const columns = Array.from({ length: columnCount }, (_, c) =>
|
||||||
|
nodes.filter((n) => n.column === c).sort((a, b) => (a.row || 0) - (b.row || 0))
|
||||||
|
)
|
||||||
|
|
||||||
|
const setNodeRef = useCallback((id) => (el) => {
|
||||||
|
if (el) nodeRefs.current[id] = el
|
||||||
|
else delete nodeRefs.current[id]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Kanten-Geometrie nach jedem Layout neu berechnen (Anker relativ zum Board)
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const container = containerRef.current
|
||||||
|
if (!container) return
|
||||||
|
const compute = () => {
|
||||||
|
const base = container.getBoundingClientRect()
|
||||||
|
const next = []
|
||||||
|
for (const node of nodes) {
|
||||||
|
for (const sourceId of node.sourceIds || []) {
|
||||||
|
const sourceEl = nodeRefs.current[sourceId]
|
||||||
|
const targetEl = nodeRefs.current[node.$id]
|
||||||
|
if (!sourceEl || !targetEl) continue
|
||||||
|
const s = sourceEl.getBoundingClientRect()
|
||||||
|
const t = targetEl.getBoundingClientRect()
|
||||||
|
next.push({
|
||||||
|
id: `${sourceId}-${node.$id}`,
|
||||||
|
x1: s.right - base.left,
|
||||||
|
y1: s.top + s.height / 2 - base.top,
|
||||||
|
x2: t.left - base.left,
|
||||||
|
y2: t.top + t.height / 2 - base.top,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEdges(next)
|
||||||
|
}
|
||||||
|
compute()
|
||||||
|
const observer = new ResizeObserver(compute)
|
||||||
|
observer.observe(container)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [nodes, columnCount])
|
||||||
|
|
||||||
|
const handleStartConnect = (node) => setConnectSource(node)
|
||||||
|
|
||||||
|
const handleSelect = async (node) => {
|
||||||
|
if (connectSource) {
|
||||||
|
if (node.column > connectSource.column) {
|
||||||
|
await onConnect(connectSource, node)
|
||||||
|
}
|
||||||
|
setConnectSource(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onSelectNode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnLabel = (c) => {
|
||||||
|
if (c === 0) return 'Start'
|
||||||
|
if (c === columnCount - 1) return 'Ziele'
|
||||||
|
return `Phase ${c}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ overflowX: 'auto', paddingBottom: 8 }}>
|
||||||
|
{connectSource && (
|
||||||
|
<div
|
||||||
|
className="muted"
|
||||||
|
style={{ fontSize: 13, marginBottom: 8, display: 'flex', alignItems: 'center', gap: 10 }}
|
||||||
|
>
|
||||||
|
Verbinden von <strong>{connectSource.title}</strong>: Zielknoten rechts davon anklicken.
|
||||||
|
<button type="button" className="ui-btn" onClick={() => setConnectSource(null)}>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: `repeat(${columnCount}, ${COLUMN_WIDTH}px)`,
|
||||||
|
gap: `16px ${COLUMN_GAP}px`,
|
||||||
|
alignItems: 'start',
|
||||||
|
minWidth: columnCount * (COLUMN_WIDTH + COLUMN_GAP),
|
||||||
|
padding: '8px 4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 0 }}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<marker id="roadmap-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
|
||||||
|
<path d="M0,0 L8,4 L0,8 Z" fill="var(--text-muted, #94a3b8)" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
{edges.map((e) => {
|
||||||
|
const dx = Math.max((e.x2 - e.x1) / 2, 24)
|
||||||
|
return (
|
||||||
|
<path
|
||||||
|
key={e.id}
|
||||||
|
d={`M ${e.x1} ${e.y1} C ${e.x1 + dx} ${e.y1}, ${e.x2 - dx} ${e.y2}, ${e.x2} ${e.y2}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--text-muted, #94a3b8)"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
markerEnd="url(#roadmap-arrow)"
|
||||||
|
opacity="0.7"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{columns.map((columnNodes, c) => (
|
||||||
|
<div key={c} style={{ display: 'flex', flexDirection: 'column', gap: 14, zIndex: 1 }}>
|
||||||
|
<div className="faint" style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||||
|
{columnLabel(c)}
|
||||||
|
</div>
|
||||||
|
{columnNodes.map((node) => (
|
||||||
|
<RoadmapNodeCard
|
||||||
|
key={node.$id}
|
||||||
|
node={node}
|
||||||
|
selected={selectedNode?.$id === node.$id}
|
||||||
|
readOnly={readOnly}
|
||||||
|
connectSource={connectSource}
|
||||||
|
isValidTarget={Boolean(connectSource) && node.column > connectSource.column}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onStartConnect={handleStartConnect}
|
||||||
|
nodeRef={setNodeRef(node.$id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!readOnly && c > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ui-btn"
|
||||||
|
style={{ borderStyle: 'dashed', fontSize: 12 }}
|
||||||
|
onClick={() => onAddNode(c, c === columnCount - 1 ? NODE_KIND.GOAL : NODE_KIND.STEP, columnNodes.length)}
|
||||||
|
>
|
||||||
|
<IconPlus size={14} /> {c === columnCount - 1 ? 'Ziel' : 'Schritt'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { IconFlag, IconPlayerPlay, IconLink } from '@tabler/icons-react'
|
||||||
|
import { NODE_KIND, NODE_STATUS, isNodeOverdue } from '../../lib/roadmaps'
|
||||||
|
|
||||||
|
const STATUS_COLORS = {
|
||||||
|
[NODE_STATUS.OPEN]: 'var(--text-muted, #94a3b8)',
|
||||||
|
[NODE_STATUS.IN_PROGRESS]: '#fbbf24',
|
||||||
|
[NODE_STATUS.DONE]: '#34d399',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Knoten-Kachel im Steckbrett.
|
||||||
|
* connectMode: Board wartet auf Zielauswahl; isValidTarget markiert erlaubte Ziele.
|
||||||
|
*/
|
||||||
|
export default function RoadmapNodeCard({
|
||||||
|
node,
|
||||||
|
selected,
|
||||||
|
readOnly,
|
||||||
|
connectSource,
|
||||||
|
isValidTarget,
|
||||||
|
onSelect,
|
||||||
|
onStartConnect,
|
||||||
|
nodeRef,
|
||||||
|
}) {
|
||||||
|
const overdue = isNodeOverdue(node)
|
||||||
|
const isStart = node.kind === NODE_KIND.START
|
||||||
|
const isGoal = node.kind === NODE_KIND.GOAL
|
||||||
|
const connecting = Boolean(connectSource)
|
||||||
|
const isConnectSource = connectSource?.$id === node.$id
|
||||||
|
|
||||||
|
const borderColor = isConnectSource
|
||||||
|
? 'var(--accent, #3b82f6)'
|
||||||
|
: connecting && isValidTarget
|
||||||
|
? '#34d399'
|
||||||
|
: selected
|
||||||
|
? 'var(--accent, #3b82f6)'
|
||||||
|
: overdue
|
||||||
|
? 'var(--danger, #ef4444)'
|
||||||
|
: 'var(--border)'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={nodeRef}
|
||||||
|
onClick={() => onSelect(node)}
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
background: isGoal ? 'rgba(59,130,246,0.10)' : 'var(--surface-2, rgba(45,55,72,0.65))',
|
||||||
|
border: `2px solid ${borderColor}`,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: '10px 12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
opacity: connecting && !isValidTarget && !isConnectSource ? 0.45 : 1,
|
||||||
|
transition: 'border-color 0.15s ease, opacity 0.15s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
{isStart && <IconPlayerPlay size={15} style={{ flexShrink: 0 }} />}
|
||||||
|
{isGoal && <IconFlag size={15} style={{ flexShrink: 0, color: '#93c5fd' }} />}
|
||||||
|
{!isStart && (
|
||||||
|
<span
|
||||||
|
title={`Status: ${node.status || 'open'}`}
|
||||||
|
style={{
|
||||||
|
width: 10, height: 10, borderRadius: 5, flexShrink: 0,
|
||||||
|
background: STATUS_COLORS[node.status] || STATUS_COLORS.open,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span style={{ fontWeight: 600, fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{node.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(node.assignedName || node.deadline) && (
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginTop: 6, fontSize: 11 }} className="faint">
|
||||||
|
{node.assignedName && <span>{node.assignedName}</span>}
|
||||||
|
{node.deadline && (
|
||||||
|
<span style={overdue ? { color: 'var(--danger, #ef4444)', fontWeight: 700 } : undefined}>
|
||||||
|
bis {node.deadline.split('-').reverse().join('.')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!readOnly && !isGoal && !connecting && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Verbindung von hier starten"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onStartConnect(node) }}
|
||||||
|
style={{
|
||||||
|
position: 'absolute', right: -11, top: '50%', transform: 'translateY(-50%)',
|
||||||
|
width: 22, height: 22, borderRadius: 11,
|
||||||
|
border: '1px solid var(--border)', background: 'var(--surface, #1e293b)',
|
||||||
|
color: 'var(--text-muted)', display: 'flex', alignItems: 'center',
|
||||||
|
justifyContent: 'center', cursor: 'crosshair', padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconLink size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/** Fortschrittsbalken fuer Roadmaps: Prozent + "x von y Knoten fertig". */
|
||||||
|
export default function RoadmapProgressBar({ progress, compact = false }) {
|
||||||
|
const { percent, done, total } = progress || { percent: 0, done: 0, total: 0 }
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, minWidth: compact ? 120 : 180 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12 }}>
|
||||||
|
<span className="muted">{percent}%</span>
|
||||||
|
{!compact && <span className="faint">{done} von {total} fertig</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ height: 6, borderRadius: 3, background: 'var(--border)', overflow: 'hidden' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: `${percent}%`,
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 3,
|
||||||
|
background: percent >= 100 ? 'var(--ok, #22c55e)' : 'var(--accent, #3b82f6)',
|
||||||
|
transition: 'width 0.3s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { useId, useState } from 'react'
|
||||||
|
|
||||||
|
/* Textfeld mit Vorschlagsliste. Freie Eingaben bleiben erlaubt;
|
||||||
|
getSuggestions(text) liefert [{ value, hint }]. */
|
||||||
|
export default function AutocompleteInput({ value, onChange, getSuggestions, id, placeholder, required }) {
|
||||||
|
const listId = useId()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [active, setActive] = useState(-1)
|
||||||
|
|
||||||
|
const items = open ? getSuggestions(value) : []
|
||||||
|
const showList = items.length > 0
|
||||||
|
|
||||||
|
const choose = (item) => {
|
||||||
|
onChange(item.value)
|
||||||
|
setOpen(false)
|
||||||
|
setActive(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
setOpen(true)
|
||||||
|
setActive((i) => Math.min(i + 1, items.length - 1))
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
setActive((i) => Math.max(i - 1, -1))
|
||||||
|
} else if (e.key === 'Enter' && showList && items[active]) {
|
||||||
|
e.preventDefault()
|
||||||
|
choose(items[active])
|
||||||
|
} else if (e.key === 'Escape' && showList) {
|
||||||
|
e.preventDefault()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="autocomplete">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id={id}
|
||||||
|
className="form-control"
|
||||||
|
role="combobox"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded={showList}
|
||||||
|
aria-controls={listId}
|
||||||
|
aria-activedescendant={showList && items[active] ? `${listId}-${active}` : undefined}
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={placeholder}
|
||||||
|
required={required}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e.target.value)
|
||||||
|
setOpen(true)
|
||||||
|
setActive(-1)
|
||||||
|
}}
|
||||||
|
onFocus={() => setOpen(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
setOpen(false)
|
||||||
|
setActive(-1)
|
||||||
|
}}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
{showList && (
|
||||||
|
<ul className="autocomplete-list" id={listId} role="listbox">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={item.value}
|
||||||
|
id={`${listId}-${index}`}
|
||||||
|
role="option"
|
||||||
|
aria-selected={index === active}
|
||||||
|
className={`autocomplete-item${index === active ? ' active' : ''}`}
|
||||||
|
// mousedown statt click: sonst verliert das Feld vorher den Fokus und die Liste schliesst
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
choose(item)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setActive(index)}
|
||||||
|
>
|
||||||
|
<span className="autocomplete-value">{item.value}</span>
|
||||||
|
{item.hint && <span className="autocomplete-hint">{item.hint}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function Badge({ tone = 'muted', dot = false, children, className = '', style }) {
|
||||||
|
return (
|
||||||
|
<span className={`badge badge-${tone} ${className}`} style={style}>
|
||||||
|
{dot && <span className="dot" />}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const VARIANTS = {
|
||||||
|
default: 'ui-btn',
|
||||||
|
primary: 'ui-btn ui-btn-primary',
|
||||||
|
ghost: 'ui-btn ui-btn-ghost',
|
||||||
|
danger: 'ui-btn ui-btn-danger',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Button({
|
||||||
|
variant = 'default',
|
||||||
|
size,
|
||||||
|
as = 'button',
|
||||||
|
className = '',
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const cls = `${VARIANTS[variant] || VARIANTS.default}${size === 'sm' ? ' ui-btn-sm' : ''} ${className}`
|
||||||
|
const Comp = as
|
||||||
|
return (
|
||||||
|
<Comp className={cls} {...props}>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default function Card({ title, actions, children, pad = true, className = '', bodyStyle, style }) {
|
||||||
|
if (!title && !actions) {
|
||||||
|
return (
|
||||||
|
<div className={`ui-card ${pad ? 'pad' : ''} ${className}`} style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={`ui-card ${className}`} style={style}>
|
||||||
|
<div className="ui-card-head">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
{actions && <div className="flex gap-2 items-center">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
<div className="ui-card-body" style={bodyStyle}>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Klick irgendwo ins Datumsfeld oeffnet den Kalender, nicht nur das kleine Symbol
|
||||||
|
const openCalendar = (e) => {
|
||||||
|
try {
|
||||||
|
e.currentTarget.showPicker?.()
|
||||||
|
} catch {
|
||||||
|
// aeltere Browser: Kalender bleibt ueber das Symbol erreichbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Datum mit Kalender und Uhrzeit nebeneinander. Werte wie die Browser-Felder:
|
||||||
|
Datum "yyyy-mm-dd", Uhrzeit "hh:mm" - umgerechnet wird erst beim Speichern. */
|
||||||
|
export default function DateTimeInput({ id, date, time, onDateChange, onTimeChange, min, required, timeRequired, timeLabel }) {
|
||||||
|
return (
|
||||||
|
<div className="datetime-pair">
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type="date"
|
||||||
|
className="form-control"
|
||||||
|
min={min || undefined}
|
||||||
|
required={required}
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => onDateChange(e.target.value)}
|
||||||
|
onClick={openCalendar}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className="form-control"
|
||||||
|
aria-label={timeLabel}
|
||||||
|
required={timeRequired}
|
||||||
|
value={time}
|
||||||
|
onChange={(e) => onTimeChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default function EmptyState({ icon, title, hint, action }) {
|
||||||
|
return (
|
||||||
|
<div className="empty">
|
||||||
|
{icon && <div className="empty-icon">{icon}</div>}
|
||||||
|
<div style={{ fontWeight: 700, marginBottom: 4 }}>{title}</div>
|
||||||
|
{hint && <div className="faint" style={{ fontSize: 13 }}>{hint}</div>}
|
||||||
|
{action && <div style={{ marginTop: 16 }}>{action}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export default function Field({ label, children, hint }) {
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
{label && <label>{label}</label>}
|
||||||
|
{children}
|
||||||
|
{hint && <div className="faint" style={{ fontSize: 12 }}>{hint}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default function Kpi({ label, value, sub, icon, accent = 'var(--accent)' }) {
|
||||||
|
return (
|
||||||
|
<div className="kpi">
|
||||||
|
<span className="kpi-accent" style={{ background: accent }} />
|
||||||
|
<div className="kpi-label">{icon}{label}</div>
|
||||||
|
<div className="kpi-value">{value}</div>
|
||||||
|
{sub && <div className="kpi-sub">{sub}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export default function PageHeader({ title, subtitle, actions, children }) {
|
||||||
|
return (
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h1 className="page-title">{title}</h1>
|
||||||
|
{subtitle && <div className="page-subtitle">{subtitle}</div>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{actions && <div className="page-actions">{actions}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { statusLabel, statusClass, isUrgent } from '../../lib/ticketStatus'
|
||||||
|
|
||||||
|
const pillBase = {
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
padding: '3px 10px',
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusPill({ status }) {
|
||||||
|
return <span className={statusClass(status)} style={pillBase}>{statusLabel(status)}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur dringende Tickets bekommen ein Abzeichen
|
||||||
|
export function UrgentPill({ priority }) {
|
||||||
|
if (!isUrgent(priority)) return null
|
||||||
|
return <span className="priority-critical" style={pillBase}>Dringend</span>
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default function Tabs({ tabs, active, onChange }) {
|
||||||
|
return (
|
||||||
|
<div className="ticket-tabs">
|
||||||
|
{tabs.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
type="button"
|
||||||
|
className={`ticket-tab ${active === t.id ? 'ticket-tab-active' : ''}`}
|
||||||
|
onClick={() => onChange(t.id)}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export { default as PageHeader } from './PageHeader'
|
||||||
|
export { default as Card } from './Card'
|
||||||
|
export { default as Button } from './Button'
|
||||||
|
export { default as Badge } from './Badge'
|
||||||
|
export { StatusPill, UrgentPill } from './StatusPill'
|
||||||
|
export { default as Tabs } from './Tabs'
|
||||||
|
export { default as EmptyState } from './EmptyState'
|
||||||
|
export { default as Field } from './Field'
|
||||||
|
export { default as Kpi } from './Kpi'
|
||||||
|
export { default as AutocompleteInput } from './AutocompleteInput'
|
||||||
|
export { default as DateTimeInput } from './DateTimeInput'
|
||||||
+92
-65
@@ -1,26 +1,68 @@
|
|||||||
import { createContext, useContext, useState, useEffect } from 'react'
|
import { createContext, useContext, useState, useEffect } from 'react'
|
||||||
import { account, databases, DATABASE_ID, COLLECTIONS, ID, Query } from '../lib/appwrite'
|
import {
|
||||||
|
account,
|
||||||
|
databases,
|
||||||
|
DATABASE_ID,
|
||||||
|
COLLECTIONS,
|
||||||
|
ID,
|
||||||
|
Query,
|
||||||
|
hasAppwriteSession,
|
||||||
|
projectId as PROJECT_ID,
|
||||||
|
} from '../lib/appwrite'
|
||||||
|
|
||||||
const AuthContext = createContext()
|
const AuthContext = createContext()
|
||||||
|
|
||||||
// Demo mode when Appwrite is not configured
|
function clearStaleAppwriteSessions() {
|
||||||
const DEMO_MODE = !import.meta.env.VITE_APPWRITE_PROJECT_ID
|
if (typeof window === 'undefined' || !window.localStorage) return
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem('cookieFallback')
|
||||||
|
if (!raw) return
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) return
|
||||||
|
for (const key of Object.keys(parsed)) {
|
||||||
|
if (key.startsWith('a_session_') && key !== `a_session_${PROJECT_ID}`) {
|
||||||
|
delete parsed[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.localStorage.setItem('cookieFallback', JSON.stringify(parsed))
|
||||||
|
} catch {
|
||||||
|
window.localStorage.removeItem('cookieFallback')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu
|
function clearAllLocalAppwriteSessions() {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
try {
|
||||||
|
window.localStorage.removeItem('cookieFallback')
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSessionWithCleanup(email, password) {
|
||||||
|
try {
|
||||||
|
await account.deleteSessions()
|
||||||
|
} catch { /* alte Sitzung ggf. nur serverseitig */ }
|
||||||
|
clearAllLocalAppwriteSessions()
|
||||||
|
if (account.createEmailPasswordSession) {
|
||||||
|
await account.createEmailPasswordSession(email, password)
|
||||||
|
} else {
|
||||||
|
await account.createEmailSession(email, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion: Fügt User automatisch zur employees Collection hinzu und liefert
|
||||||
|
// den Mitarbeiter-Eintrag (Kürzel = Gitea-Name, siehe isWorksheetCreator) oder null
|
||||||
async function ensureEmployeeExists(user) {
|
async function ensureEmployeeExists(user) {
|
||||||
if (!user || DEMO_MODE) return
|
if (!user) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prüfe ob User bereits in employees Collection existiert
|
|
||||||
const response = await databases.listDocuments(
|
const response = await databases.listDocuments(
|
||||||
DATABASE_ID,
|
DATABASE_ID,
|
||||||
COLLECTIONS.EMPLOYEES,
|
COLLECTIONS.EMPLOYEES,
|
||||||
[Query.equal('userId', user.$id)]
|
[Query.equal('userId', user.$id)]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wenn User noch nicht existiert, füge ihn hinzu
|
|
||||||
if (response.documents.length === 0) {
|
if (response.documents.length === 0) {
|
||||||
await databases.createDocument(
|
const employee = await databases.createDocument(
|
||||||
DATABASE_ID,
|
DATABASE_ID,
|
||||||
COLLECTIONS.EMPLOYEES,
|
COLLECTIONS.EMPLOYEES,
|
||||||
ID.unique(),
|
ID.unique(),
|
||||||
@@ -28,21 +70,24 @@ async function ensureEmployeeExists(user) {
|
|||||||
userId: user.$id,
|
userId: user.$id,
|
||||||
displayName: user.name || user.email,
|
displayName: user.name || user.email,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
shortcode: '' // Kürzel wird später vom Admin hinzugefügt
|
shortcode: ''
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
console.log('✅ User automatisch zur Mitarbeiter-Liste hinzugefügt')
|
console.log('✅ User automatisch zur Mitarbeiter-Liste hinzugefügt')
|
||||||
|
return employee
|
||||||
}
|
}
|
||||||
|
return response.documents[0]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fehler ignorieren wenn Collection nicht existiert oder Permissions fehlen
|
|
||||||
if (error.code !== 404) {
|
if (error.code !== 404) {
|
||||||
console.warn('Could not add user to employees collection:', error.message)
|
console.warn('Could not add user to employees collection:', error.message)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuthProvider({ children }) {
|
export function AuthProvider({ children }) {
|
||||||
const [user, setUser] = useState(null)
|
const [user, setUser] = useState(null)
|
||||||
|
const [employee, setEmployee] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,27 +95,23 @@ export function AuthProvider({ children }) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function checkUser() {
|
async function checkUser() {
|
||||||
if (DEMO_MODE) {
|
if (!hasAppwriteSession()) {
|
||||||
// Check localStorage for demo session
|
setUser(null)
|
||||||
const demoUser = localStorage.getItem('demo_user')
|
|
||||||
if (demoUser) {
|
|
||||||
setUser(JSON.parse(demoUser))
|
|
||||||
}
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await account.get()
|
const session = await account.get()
|
||||||
setUser(session)
|
setUser(session)
|
||||||
// Automatisch zur employees Collection hinzufügen
|
setEmployee(await ensureEmployeeExists(session))
|
||||||
await ensureEmployeeExists(session)
|
|
||||||
} catch (error) {
|
} 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) {
|
if (error.code !== 401 && error.code !== 404) {
|
||||||
console.error('Unexpected error checking user:', error)
|
console.error('Unexpected error checking user:', error)
|
||||||
}
|
}
|
||||||
|
if (error.code === 401) {
|
||||||
|
clearAllLocalAppwriteSessions()
|
||||||
|
}
|
||||||
setUser(null)
|
setUser(null)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -78,78 +119,64 @@ export function AuthProvider({ children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login(email, password) {
|
async function login(email, password) {
|
||||||
if (DEMO_MODE) {
|
|
||||||
// Demo login - accept any credentials
|
|
||||||
const demoUser = { $id: 'demo', email, name: email.split('@')[0] }
|
|
||||||
localStorage.setItem('demo_user', JSON.stringify(demoUser))
|
|
||||||
setUser(demoUser)
|
|
||||||
return { success: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Appwrite 1.5.7 / SDK 13.0 - versuche beide Methoden für Kompatibilität
|
const normalizedEmail = email.trim()
|
||||||
try {
|
const normalizedPassword = password.trim()
|
||||||
await account.createEmailSession(email, password)
|
clearStaleAppwriteSessions()
|
||||||
} catch (e) {
|
|
||||||
// Fallback für ältere API
|
await createSessionWithCleanup(normalizedEmail, normalizedPassword)
|
||||||
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()
|
const session = await account.get()
|
||||||
setUser(session)
|
setUser(session)
|
||||||
await ensureEmployeeExists(session)
|
setEmployee(await ensureEmployeeExists(session))
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error)
|
console.error('Login error:', error)
|
||||||
let errorMessage = error.message || 'Login fehlgeschlagen'
|
let errorMessage = error.message || 'Login fehlgeschlagen'
|
||||||
|
|
||||||
// Bessere Fehlermeldungen
|
if (error.code === 429 || errorMessage.includes('Rate limit')) {
|
||||||
if (error.code === 401 || errorMessage.includes('Invalid credentials')) {
|
errorMessage = 'Zu viele Login-Versuche. Bitte 15–30 Minuten warten und es erneut versuchen.'
|
||||||
|
} else if (errorMessage.includes('missing scopes') && errorMessage.includes('account')) {
|
||||||
|
errorMessage = 'Session konnte nicht gespeichert werden. Bitte Seite neu laden und erneut versuchen.'
|
||||||
|
} else if (
|
||||||
|
error?.type === 'user_session_already_exists' ||
|
||||||
|
errorMessage.includes('session is active')
|
||||||
|
) {
|
||||||
|
errorMessage = 'Es gibt noch eine alte Sitzung. Bitte Seite neu laden und erneut anmelden.'
|
||||||
|
} else if (error.code === 401 || errorMessage.includes('Invalid credentials')) {
|
||||||
errorMessage = 'Ungültige Email oder Passwort'
|
errorMessage = 'Ungültige Email oder Passwort'
|
||||||
} else if (errorMessage.includes('User not found')) {
|
} else if (errorMessage.includes('User not found')) {
|
||||||
errorMessage = 'Benutzer nicht gefunden. Bitte registriere dich zuerst.'
|
errorMessage = 'Benutzer nicht gefunden. Bitte registriere dich zuerst.'
|
||||||
} else if (errorMessage.includes('Email/Password')) {
|
} else if (errorMessage.includes('Email/Password')) {
|
||||||
errorMessage = 'Email/Password Authentifizierung ist nicht aktiviert. Bitte aktiviere sie in deinem Appwrite Dashboard unter Auth → Providers.'
|
errorMessage = 'Email/Password Authentifizierung ist nicht aktiviert. Bitte aktiviere sie in deinem Appwrite Dashboard unter Auth → Providers.'
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: errorMessage }
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
if (DEMO_MODE) {
|
|
||||||
localStorage.removeItem('demo_user')
|
|
||||||
setUser(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await account.deleteSession('current')
|
await account.deleteSessions()
|
||||||
setUser(null)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error)
|
console.error('Logout error:', error)
|
||||||
|
} finally {
|
||||||
|
clearAllLocalAppwriteSessions()
|
||||||
|
setUser(null)
|
||||||
|
setEmployee(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilfsfunktion um zu prüfen ob Benutzer Admin ist
|
const isAdmin = Boolean(user?.labels?.includes('admin'))
|
||||||
const isAdmin = () => {
|
|
||||||
if (!user) return false
|
|
||||||
// Prüfe ob Benutzer das "admin" Label hat
|
|
||||||
return user.labels?.includes('admin') || false
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
user,
|
user,
|
||||||
|
employee,
|
||||||
loading,
|
loading,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
isAdmin: isAdmin()
|
isAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user