Compare commits
313
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f11fd0bb25 | ||
|
|
8685411a93 | ||
|
|
1742fb254d | ||
|
|
06166221ec | ||
|
|
8f75276c54 | ||
|
|
dce7c3e53a | ||
|
|
f0d39f2eee | ||
|
|
5e59ad44bd | ||
|
|
58bb956645 | ||
|
|
463aaf1066 | ||
|
|
cbf4b265e0 | ||
|
|
45b2b145b0 | ||
|
|
0112c77afc | ||
|
|
dc4df16cab | ||
|
|
d01a804cea | ||
|
|
8faf61e8bb | ||
|
|
38020bfdff | ||
|
|
514d1c2d4e | ||
|
|
4b184f744f | ||
|
|
79f230ea2c | ||
|
|
07d36f64e9 | ||
|
|
b556a459bb | ||
|
|
dec7bea7d2 | ||
|
|
b7b3e834f8 | ||
|
|
05170068bc | ||
|
|
fbd3990e64 | ||
|
|
af43c70288 | ||
|
|
f89ecec8ff | ||
|
|
62b64bb357 | ||
|
|
65526ca9b3 | ||
|
|
2aa4abeadb | ||
|
|
746244a4a6 | ||
|
|
5060d150ea | ||
|
|
8ab78b7b2c | ||
|
|
3650573445 | ||
|
|
6a5f943034 | ||
|
|
8b055a5d42 | ||
|
|
0f1dbabd98 | ||
|
|
e37c0a664e | ||
|
|
cc95018083 | ||
|
|
45dd73d8cc | ||
|
|
241f7cdec6 | ||
|
|
674cce26f0 | ||
|
|
b2a0ff5d53 | ||
|
|
99ab4bd47b | ||
|
|
8f90fdc05a | ||
|
|
e1c9b57249 | ||
|
|
312705efc4 | ||
|
|
e7bd146641 | ||
|
|
e116e804e3 | ||
|
|
eaf9fce1bc | ||
|
|
d2883018fc | ||
|
|
5d8130e8ef | ||
|
|
fa1ce2c4cb | ||
|
|
ab4a300e70 | ||
|
|
bca99d4790 | ||
|
|
6adeac1642 | ||
|
|
0a881fce8f | ||
|
|
84186d1429 | ||
|
|
a9fa9f58e6 | ||
|
|
8f5013aafc | ||
|
|
38beaead99 | ||
|
|
313ead77f4 | ||
|
|
625efc7f18 | ||
|
|
3b26fcdf0b | ||
|
|
d1a1046969 | ||
|
|
b5d27d5801 | ||
|
|
bd1eccb4e3 | ||
|
|
6d015df6fa | ||
|
|
e1932a2a48 | ||
|
|
a2a6d83b15 | ||
|
|
3e5839bee6 | ||
|
|
5e5093b7f2 | ||
|
|
a505d38c96 | ||
|
|
6eae3c4aae | ||
|
|
568911967d | ||
|
|
2f6437ee42 | ||
|
|
b369a0edf9 | ||
|
|
c96aa870a9 | ||
|
|
cb7ac6edf6 | ||
|
|
ea528486aa | ||
|
|
308bf545da | ||
|
|
b81e53eef5 | ||
|
|
910fa0ff30 | ||
|
|
f703c88c90 | ||
|
|
816d4552fa | ||
|
|
8b688f9df9 | ||
|
|
860079f51d | ||
|
|
f1aac2ada9 | ||
|
|
47cf82eb91 | ||
|
|
8807af3f42 | ||
|
|
154aea44a8 | ||
|
|
92fef9d4a5 | ||
|
|
fa2a66a58f | ||
|
|
9da42d52c3 | ||
|
|
5647412e34 | ||
|
|
d2d4ad9cfb | ||
|
|
bea91d9beb | ||
|
|
b24121e34f | ||
|
|
8c34e932c1 | ||
|
|
8ef47713b5 | ||
|
|
7d4676121e | ||
|
|
4b6ab7505a | ||
|
|
35e2761e68 | ||
|
|
7a3bc2887e | ||
|
|
f67192db52 | ||
|
|
8319084930 | ||
|
|
416c79b3a6 | ||
|
|
e54a30b006 | ||
|
|
a0149137ea | ||
|
|
dd0bef52e8 | ||
|
|
f6b329b208 | ||
|
|
ed8bdba015 | ||
|
|
504571c0ce | ||
|
|
ff999ae1b3 | ||
|
|
30b3caee34 | ||
|
|
2ecc0b627a | ||
|
|
b544b83da0 | ||
|
|
200d68f20d | ||
|
|
39dfb386ac | ||
|
|
6eb5ab3679 | ||
|
|
ab42918bbc | ||
|
|
0b53fc825a | ||
|
|
9fec381056 | ||
|
|
32746727ed | ||
|
|
54fc44ed11 | ||
|
|
f3b73ca1dd | ||
|
|
8d75f7346e | ||
|
|
f911dbcb03 | ||
|
|
47a26fb9b2 | ||
|
|
988934552f | ||
|
|
769ec70847 | ||
|
|
822387e4f7 | ||
|
|
5a0307c9d6 | ||
|
|
c19e19968d | ||
|
|
c18a29c748 | ||
|
|
2e82be3c1a | ||
|
|
bb110a734e | ||
|
|
3179b9cb41 | ||
|
|
74e70c654f | ||
|
|
18e8f3bda5 | ||
|
|
e28162eabf | ||
|
|
9a60698a87 | ||
|
|
4af78f0bad | ||
|
|
9d604aa550 | ||
|
|
00fa229235 | ||
|
|
919d96f7a0 | ||
|
|
0a2eab85d5 | ||
|
|
3d67aa88cf | ||
|
|
4c3f67eadc | ||
|
|
878a530bd7 | ||
|
|
994347408d | ||
|
|
72eedeb59c | ||
|
|
89f5bd4bc6 | ||
|
|
225e2f1b74 | ||
|
|
45e5dc30dc | ||
|
|
857e001911 | ||
|
|
c0015544c4 | ||
|
|
603d08c9c2 | ||
|
|
6f0371283e | ||
|
|
c262268015 | ||
|
|
f4c8b235b9 | ||
|
|
78bc99a73b | ||
|
|
65e9931f2c | ||
|
|
426a5d6654 | ||
|
|
36eb55b81b | ||
|
|
feda436936 | ||
|
|
281c49d406 | ||
|
|
109aef6805 | ||
|
|
cef0d5465e | ||
|
|
91d526e909 | ||
|
|
813c6a7e18 | ||
|
|
eb0b889203 | ||
|
|
005875d970 | ||
|
|
717a3323fa | ||
|
|
fce2f41463 | ||
|
|
370724a9a7 | ||
|
|
8c7a3fcced | ||
|
|
52968b3017 | ||
|
|
72f07410fe | ||
|
|
1e72faf97c | ||
|
|
e9cfae28ee | ||
|
|
0b1101d384 | ||
|
|
edf118a044 | ||
|
|
2aacdb2d56 | ||
|
|
24a3cc41ef | ||
|
|
1feeb18c08 | ||
|
|
18bc3012e0 | ||
|
|
3826f1ab72 | ||
|
|
88a98b858f | ||
|
|
0069a62a09 | ||
|
|
c910bedcec | ||
|
|
8ecf07fecd | ||
|
|
0cec45d3da | ||
|
|
447b7e733e | ||
|
|
c1818fadcf | ||
|
|
2de846436b | ||
|
|
0f884e41e8 | ||
|
|
471c7a056d | ||
|
|
c16049d266 | ||
|
|
8653756935 | ||
|
|
7e79dac6b4 | ||
|
|
8185081921 | ||
|
|
9dc447e92c | ||
|
|
24e4f054c4 | ||
|
|
bc9d673ff7 | ||
|
|
d83bbfb48d | ||
|
|
3535f18a2b | ||
|
|
32bc3d7c3d | ||
|
|
1497f7a4dc | ||
|
|
564fecdde4 | ||
|
|
a336dc07fc | ||
|
|
03af5fcfb8 | ||
|
|
57f235dd36 | ||
|
|
16f3150ff1 | ||
|
|
ec740ca970 | ||
|
|
ae905a5d9b | ||
|
|
3eb67bba81 | ||
|
|
6323b830d7 | ||
|
|
fa23732217 | ||
|
|
82c2460835 | ||
|
|
dddfb02d1d | ||
|
|
b8e4de97c7 | ||
|
|
21325c4037 | ||
|
|
3af8a2f2ad | ||
|
|
3a3e43d5ec | ||
|
|
3e3a9a3bbd | ||
|
|
56489b8d6d | ||
|
|
cc59e7978e | ||
|
|
c24b3d43c8 | ||
|
|
c8c1d84794 | ||
|
|
c7c1e338a3 | ||
|
|
67b9439145 | ||
|
|
31be2af4c3 | ||
|
|
1212173cb4 | ||
|
|
0105179e21 | ||
|
|
701ff1a8e8 | ||
|
|
f120ec2ef3 | ||
|
|
3b6a821687 | ||
|
|
b05f49621c | ||
|
|
7b31cb5368 | ||
|
|
50d237a932 | ||
|
|
92fdfdc961 | ||
|
|
5a066eb3c4 | ||
|
|
31144148d1 | ||
|
|
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 | ||
|
|
a4c64b5398 | ||
|
|
cb110a184b | ||
|
|
99b89bcabe | ||
|
|
895c55399f | ||
|
|
ee7c866616 | ||
|
|
5717612db5 | ||
|
|
3f8fce3c02 |
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
|
||||
node_modules/
|
||||
|
||||
# Environment variables
|
||||
# Environment variables (persönliche Overrides — nicht committen)
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
.env.*.local
|
||||
|
||||
# Build output
|
||||
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
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
Sehr geehrtes Hetzner Team,
|
||||
|
||||
bezüglich der Portscan Erkennung haben wir folgende Präventionsmaßnahmen implementiert.
|
||||
|
||||
Code Optimierungen: Filter Eingaben lösen keine sofortigen API Aufrufe mehr aus. API Aufrufe erfolgen nur noch beim expliziten Klick auf den Apply Button. Dies reduziert unnötige TCP Verbindungen um etwa 90 Prozent.
|
||||
|
||||
Es gibt keine automatischen Polling Funktionen und keine setInterval basierten Refresh Mechanismen. API Aufrufe erfolgen nur bei Benutzerinteraktionen.
|
||||
|
||||
Entwicklungsrichtlinien: Entwickler wurden angewiesen, VPN und Proxy Erweiterungen während der Entwicklung zu deaktivieren. Security Plugins werden vor dem Testen überprüft.
|
||||
|
||||
Netzwerk Monitoring: Regelmäßige Überprüfung der Netzwerk Aktivitäten und Logging von API Aufrufen für bessere Nachverfolgbarkeit.
|
||||
|
||||
Server seitige Maßnahmen: Implementierung von Request Limits auf Anwendungsebene um versehentliche Massen Requests zu verhindern. Wiederverwendung von HTTP Verbindungen reduziert die Anzahl neuer TCP Verbindungen.
|
||||
|
||||
Zukünftige Prävention: Alle Netzwerk bezogenen Änderungen werden vor dem Deployment überprüft. Automatische Tests für Netzwerk Verhalten werden durchgeführt. Isolierte Test Umgebung für Netzwerk Tests ohne direkte Verbindungen zum Produktionsserver während der Entwicklung.
|
||||
|
||||
Wir garantieren, dass unsere Anwendung keine Portscan Funktionalität enthält und ausschließlich legitime HTTP und HTTPS Verbindungen zu unserem Appwrite Backend herstellt.
|
||||
|
||||
Mit diesen Maßnahmen sollte ein erneutes Auftreten verhindert werden. Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
@@ -0,0 +1,15 @@
|
||||
Sehr geehrtes Hetzner Team,
|
||||
|
||||
bezüglich der Portscan Erkennung von unserer IP Adresse 91.99.156.85 am 30.12.2025 um 10:59:37 UTC möchten wir die Ursache erläutern.
|
||||
|
||||
Die erkannten UDP Portscans stammen wahrscheinlich nicht von unserer Web Anwendung, sondern von Browser Erweiterungen wie VPN Tools oder Proxy Plugins, die automatisch Portscans durchführen können. Diese laufen im Hintergrund und sind dem Benutzer oft nicht bewusst.
|
||||
|
||||
Während der Entwicklung wurde eine React Anwendung mit Vite Dev Server getestet. Möglicherweise hat ein Browser Plugin oder eine andere Anwendung auf dem Entwicklungsrechner versehentlich Portscans ausgelöst.
|
||||
|
||||
Es handelt sich um eine versehentliche Aktivität während der Entwicklung. Es gab keine absichtliche Portscan Aktivität oder Angriffsversuche.
|
||||
|
||||
Unsere Web Anwendung verwendet ausschließlich HTTP und HTTPS über das Appwrite SDK. Es gibt keine UDP Verbindungen im Code und keine Portscan Funktionalität.
|
||||
|
||||
Wir bitten um Entsperrung unserer IP Adresse 91.99.156.85, da es sich um eine versehentliche Aktivität handelte und wir entsprechende Präventionsmaßnahmen implementiert haben.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
@@ -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,44 @@
|
||||
# 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 Finanzen (Rechnungen
|
||||
schreiben und verwalten, Buchungen zuordnen; nur für Admins).
|
||||
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
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
- Lokal starten: siehe [LOCAL_DEV.md](./LOCAL_DEV.md) (Vite-Proxy + `.env.local`).
|
||||
- Live ist der Branch `test` im Gitea-Repo `JUSN/tickte-system`. Ein Push löst
|
||||
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:
|
||||
1. Erstelle ein Appwrite-Projekt auf [cloud.appwrite.io](https://cloud.appwrite.io)
|
||||
2. Erstelle eine `.env` Datei im Root-Verzeichnis:
|
||||
```env
|
||||
VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
||||
VITE_APPWRITE_PROJECT_ID=deine-project-id-hier
|
||||
VITE_APPWRITE_DATABASE_ID=woms-database
|
||||
```
|
||||
3. Folge der detaillierten Anleitung in `APPWRITE_SETUP.md` um die Datenbank-Struktur zu erstellen
|
||||
| Collection | Inhalt | Wichtige Felder |
|
||||
|---|---|---|
|
||||
| `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` |
|
||||
| `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) |
|
||||
| `customers` | Kunden und Leads | `code` (Kundennummer), `name`/`companyName`, `customerStatus` (z. B. lead, customer, lost), `archived`, Rechnungsadresse, Portal-Zugang, `invoiceNinjaClientId` (nicht mehr genutzt, Daten bleiben) |
|
||||
| `employees` | Mitarbeiter | `userId` (Appwrite-Benutzer), `displayName`, `email`, `shortcode` (Kürzel) |
|
||||
| `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 |
|
||||
| `roadmaps`, `roadmapNodes` | alte Projektpläne | wird von WOMS nicht mehr gelesen (Plan mit Schritten, verknüpft mit einem Ticket) |
|
||||
| `config` | alte Einstellungen | wird von WOMS nicht mehr gelesen (Prioritäten, alte Listen) |
|
||||
|
||||
### 3. App starten
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Zusammenspiel mit anderen Systemen:
|
||||
|
||||
Die App läuft dann auf `http://localhost:5173`
|
||||
|
||||
## 📚 Dokumentation
|
||||
|
||||
- **[APPWRITE_SETUP.md](./APPWRITE_SETUP.md)** - Detaillierte Anleitung zur Appwrite-Einrichtung
|
||||
- Schritt-für-Schritt Setup
|
||||
- Datenbank-Schema
|
||||
- Berechtigungen
|
||||
- Fehlerbehebung
|
||||
|
||||
## Features
|
||||
|
||||
- Ticket-Erstellung und -Verwaltung
|
||||
- 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
|
||||
- **Kundenportal** (project.webklar.com): legt bei jedem Git-Push ein Arbeitsblatt an
|
||||
(`employeeId` `gitea`, `serviceType` `GIT`, ohne Beginn). Die Zeit wird in WOMS
|
||||
nachgetragen, danach zählt sie als Arbeitszeit. Termine aus der Rechnungsanalyse
|
||||
landen am Akquise-Ticket des Kunden und erscheinen im Planboard. Kunden-, Mitarbeiter-
|
||||
und Projektverwaltung laufen über die Admin-API des Portals.
|
||||
- **Finanzen-API des Portals** (`/api/admin/finanzen`): Übersicht, Rechnungen (Stripe),
|
||||
Buchungen (Bank), Einstellungen und Fixkosten. WOMS spricht sie mit dem Appwrite-JWT an
|
||||
und zeigt sie unter `/finance` (Einstellungen: `/finance/einstellungen`).
|
||||
- **ticket-integrations** (`/api/integrations`): wird von WOMS nicht mehr genutzt
|
||||
(weder InvoiceNinja noch Paperless; Paperless selbst ist gestoppt).
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
# Mitarbeiter-2FA per E-Mail-Code + Wechsel Ticketsystem ⇄ Projekt-Admin – Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Mitarbeiter melden sich in Ticketsystem und Projekt-Admin nur mit Passwort + E-Mail-Code an (Appwrite-MFA), und ein Seitenleisten-Eintrag wechselt ohne zweiten Login ins Projekt-Admin, ein Kopfzeilen-Button zurück.
|
||||
|
||||
**Architecture:** Appwrite 1.8.1 erzwingt den zweiten Faktor (`user.mfa` + `emailVerification`). Das Ticketsystem (React, SDK 13) ruft die MFA-Endpunkte über `client.call` auf; das Kundenportal (Express) führt beim Mitarbeiter-Login die Challenge serverseitig mit der Appwrite-Sitzung aus der Login-Antwort durch. Der Wechsel nutzt die vorhandenen Einmal-Tokens aus `staffPreviewTokens.js` mit neuem `purpose: 'switch'`.
|
||||
|
||||
**Tech Stack:** Kundenportal: Node/Express, `node --test`, Appwrite-Nachbau `test/support/appwriteMock.js`. Ticketsystem: React 18 + Vite, Appwrite-Web-SDK 13.0.2, neu `node --test` für reine Module; UI-Prüfung per esbuild-Harness + Playwright im Scratchpad (Rezept: Memory „woms-ticketsystem“, `MemoryMax` beachten).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-30-mitarbeiter-2fa-und-wechsel-design.md` (dieser Worktree)
|
||||
|
||||
**Repos / Worktrees:**
|
||||
- Ticketsystem: `/home/webklar/apps/ticket.webklar.com/dev-2fa`, Branch `feature/mitarbeiter-2fa` (von `origin/test` 3b6a821). `node_modules` ist getrackt → nur `git add <dateien>`, nie `-A`. **Nie pushen** (Webhook deployt jeden Branch).
|
||||
- Kundenportal: `/home/webklar/apps/kundenbereich.webklar.com/dev-2fa`, Branch `feature/mitarbeiter-2fa` von `origin/main` (d5c70a5), anlegen in Task 1 (`git -C build worktree add -b feature/mitarbeiter-2fa ../dev-2fa origin/main`, `node_modules` als Symlink auf `../build/node_modules`). Nicht pushen.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Appwrite-Endpunkte (1.8.1): `POST /v1/account/mfa/challenges {factor:'email'}` → `{ $id, expire }`; `PUT /v1/account/mfa/challenges {challengeId, otp}`; Admin: `PATCH /v1/users/:id/mfa {mfa}`, `PATCH /v1/users/:id/verification {emailVerification}`.
|
||||
- Fehlertypen: `user_more_factors_required` (401, halbe Sitzung), `user_invalid_token` (401, Code falsch/abgelaufen), `user_email_not_verified`, `general_rate_limit_exceeded` / Status 429.
|
||||
- Code gilt 1 Stunde (Appwrite). Offene Portal-Code-Anmeldung: 10 Minuten, max. 5 Fehlversuche.
|
||||
- Einmal-Link: 60 s, einmalig, `purpose: 'switch'`; Preview-Tokens ohne `purpose` gelten als `'preview'`.
|
||||
- MFA-Aufrufe senden `X-Appwrite-Locale: de`.
|
||||
- Texte (wörtlich, in beiden Systemen gleich):
|
||||
- Hinweis: `Wir haben dir einen Code an {maskierteEmail} geschickt.`
|
||||
- Feld-Label: `Code aus der E-Mail`; Buttons: `Anmelden`, `Code erneut senden`, `Abbrechen`, (halbe Sitzung beim Laden) `Code senden`
|
||||
- `user_invalid_token` → `Code falsch oder abgelaufen.`
|
||||
- 429 → `Zu viele Versuche. Bitte warte einige Minuten.`
|
||||
- `user_email_not_verified` → `2FA ist für dein Konto noch nicht eingerichtet – bitte Kenso Bescheid geben.`
|
||||
- Portal nach 5 Fehlversuchen / abgelaufen → `Bitte melde dich erneut an.`
|
||||
- E-Mail-Maskierung: erstes Zeichen des lokalen Teils + `…@` + Domain (`kenso@webklar.com` → `k…@webklar.com`).
|
||||
- Seitenleiste: Label `Projekt-Admin`, Icon `IconExternalLink`; Kopfzeile Portal: `← Ticketsystem` → `https://ticket.webklar.com/tickets`.
|
||||
- Kunden-Login und Mitarbeiter mit `mfa = false` verhalten sich exakt wie heute.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Portal-Umgehung:** Mitarbeiter mit `mfa = true` darf nach `POST /api/auth/login` **kein** `webklar_portal_session`- und kein Preview-Cookie bekommen – Test in Task 3.
|
||||
2. **Reload mit halber Sitzung im Ticketsystem:** darf keine Mail automatisch auslösen und nicht in einer Login-Schleife enden – Test in Task 6 (UI-Harness).
|
||||
3. **Token-Verwechslung:** Ein Switch-Token darf nicht über `/api/auth/staff-preview` einlösbar sein und umgekehrt – Test in Task 4.
|
||||
4. **Halbe Sitzung beim Wechsel:** JWT aus halber Sitzung → `/account` 401 → `switch-link` 401, kein Token – Test in Task 4.
|
||||
5. **Abbrechen/Neu-Login mit halber Sitzung im Ticketsystem:** `deleteSessions()` vor dem Neu-Login muss auch mit halber Sitzung klappen (in Appwrite zur MFA-Gruppe gehörend? – wenn nicht, wird der Fehler geschluckt wie heute und die lokale Sitzung trotzdem gelöscht) – Test in Task 5.
|
||||
|
||||
---
|
||||
|
||||
## Teil A – Kundenportal (`kundenbereich.webklar.com/dev-2fa`)
|
||||
|
||||
### Task 1: Appwrite-Sitzung aus dem Login + MFA-Aufrufe im Namen der Sitzung
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/services/appwriteClient.js` (`appwriteFetch`, `loginWithAppwrite`)
|
||||
- Modify: `test/support/appwriteMock.js` (Sessions + MFA-Nachbau)
|
||||
- Test: `test/staffMfa.test.js` (neu, erster Teil)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `loginWithAppwrite(email, password) → { $id, email, name: '', sessionCookie: string }` (`sessionCookie` = Wert von `a_session_<projectId>` aus `set-cookie`, sonst `''`).
|
||||
- `appwriteSessionFetch(path, { method, body, sessionCookie }) → data` – wie `appwriteFetch`, zusätzlich Header `Cookie: a_session_<projectId>=<sessionCookie>` und `X-Appwrite-Locale: de`; Fehler mit `.status`, `.type` wie `appwriteFetch`.
|
||||
- Mock: `POST /v1/account/sessions/email` (setzt `set-cookie a_session_<project>=sess-<userId>`; Nutzer aus `mock.addUser({ $id, email, password, mfa, emailVerification, labels })`), `GET /v1/users/:id` liefert `mfa`, `emailVerification`, `labels`, `email`; `GET /v1/account` mit Cookie → 401 `user_more_factors_required` solange mfa und Faktor fehlt; `POST/PUT /v1/account/mfa/challenges` (OTP abrufbar über `mock.lastOtp(userId)`, `mock.expireChallenges()`); `PATCH /v1/users/:id/mfa`, `PATCH /v1/users/:id/verification`; Zähler `mock.mailsSent(userId)`.
|
||||
|
||||
- [ ] **Step 1:** Worktree anlegen (siehe Kopf), `npm test` im Worktree → alle bestehenden Tests grün (Ausgangslage notieren).
|
||||
- [ ] **Step 2: Failing test** in `test/staffMfa.test.js`:
|
||||
- `loginWithAppwrite liefert sessionCookie` → `assert.equal(user.sessionCookie, 'sess-u_staff')`.
|
||||
- `appwriteSessionFetch schickt Cookie und Locale` → Challenge anlegen, `mock.mailsSent('u_staff') === 1`, Mock hat Header `x-appwrite-locale: de` gesehen.
|
||||
- [ ] **Step 3:** `npm test` → FAIL (`sessionCookie` undefined / Funktion fehlt).
|
||||
- [ ] **Step 4:** Implementieren: `appwriteFetch` gibt intern auch die Response-Header zurück (`response.headers.getSetCookie()`), `loginWithAppwrite` nimmt das Cookie `a_session_${config.appwrite.projectId}`; `appwriteSessionFetch` exportieren. Mock erweitern.
|
||||
- [ ] **Step 5:** `npm test` → PASS (alle).
|
||||
- [ ] **Step 6: Commit** `git add server/services/appwriteClient.js test/support/appwriteMock.js test/staffMfa.test.js && git commit -m "Portal-Login: Appwrite-Sitzung merken, MFA-Aufrufe im Namen der Sitzung"`
|
||||
|
||||
### Task 2: Offene Code-Anmeldungen (`staffMfa.js`)
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/staffMfa.js`
|
||||
- Test: `test/staffMfa.test.js` (erweitern)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `appwriteSessionFetch` (Task 1).
|
||||
- Produces:
|
||||
- `maskEmail(email: string) → string`
|
||||
- `startStaffMfa({ userId, email, sessionCookie }) → Promise<{ key: string, maskedEmail: string }>` – legt Challenge an, Eintrag `{ userId, email, sessionCookie, challengeId, expiresAt: now+10min, attempts: 0 }` in einer `Map`.
|
||||
- `verifyStaffMfa(key, otp) → Promise<{ ok: true, userId, email } | { ok: false, reason: 'invalid' | 'gone' }>` – `invalid` bei `user_invalid_token` (attempts+1; beim 5. Fehlversuch Eintrag löschen → danach `gone`), `gone` bei unbekanntem/abgelaufenem Schlüssel; bei Erfolg Eintrag löschen. Andere Fehler (z. B. 429) werden geworfen.
|
||||
- `resendStaffMfa(key) → Promise<boolean>` – neue Challenge, `challengeId` ersetzen; `false` wenn Eintrag weg.
|
||||
- `MFA_COOKIE = 'webklar_portal_mfa'`
|
||||
|
||||
- [ ] **Step 1: Failing tests:**
|
||||
- `maskEmail`: `'kenso@webklar.com' → 'k…@webklar.com'`, `'a@b.de' → 'a…@b.de'`, `'' → ''`.
|
||||
- `richtiger Code`: start → `verifyStaffMfa(key, mock.lastOtp('u_staff'))` → `{ ok: true, userId: 'u_staff', email }`; zweiter Aufruf mit gleichem Key → `{ ok:false, reason:'gone' }`.
|
||||
- `falscher Code 5x`: vier Mal `reason:'invalid'`, fünfter `invalid`, sechster `gone`.
|
||||
- `abgelaufen`: Zeit per `Date.now`-Stub +11 min → `gone`.
|
||||
- `resend`: `mock.mailsSent` steigt auf 2, alter OTP ungültig (`invalid`), neuer gültig.
|
||||
- [ ] **Step 2:** `npm test` → FAIL (Modul fehlt).
|
||||
- [ ] **Step 3:** `server/services/staffMfa.js` implementieren (Schlüssel `randomBytes(32).toString('hex')`, Aufräumen wie `staffPreviewTokens.purgeExpired`).
|
||||
- [ ] **Step 4:** `npm test` → PASS.
|
||||
- [ ] **Step 5: Commit** `git add server/services/staffMfa.js test/staffMfa.test.js && git commit -m "Portal: offene Mitarbeiter-Code-Anmeldungen (staffMfa)"`
|
||||
|
||||
### Task 3: Login-Route mit Code-Schritt + Login-Seite
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/staffAccount.js` (verschiebt `resolveStaffAccount` aus `routes/auth.js` unverändert, exportiert)
|
||||
- Modify: `server/routes/auth.js` (Login, neue Routen, Helfer `startStaffPortalSession`)
|
||||
- Modify: `public/login.html`, `public/app.js` (`initLoginPage`), `public/login.css` (nur falls nötig)
|
||||
- Test: `test/staffLogin.test.js` (neu, mit `startTestApp({ '/api/auth': authRoutes })`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 + 2.
|
||||
- Produces:
|
||||
- `resolveStaffAccount(appwriteUserId, email, account) → { isAdmin, name, email } | null` (aus `server/services/staffAccount.js`).
|
||||
- In `auth.js` lokal: `startStaffPortalSession(res, { userId, staff }) → Promise<void>` (setzt Portal-Sitzung + Preview-Cookie exakt wie heute der Mitarbeiter-Zweig; wird in Task 4 von `staff-switch` genutzt – deshalb exportieren als benannten Export).
|
||||
- `POST /api/auth/login`: Mitarbeiter mit `account.mfa === true` → Status 200 `{ mfaRequired: true, email: maskedEmail }` + signiertes httpOnly-Cookie `webklar_portal_mfa` (10 min, `sameSite: 'lax'`, `secure` wie Portal-Cookie); **keine** Portal-Sitzung.
|
||||
- `POST /api/auth/login/mfa {otp}` → 200 `{ success:true, role:'admin', admin }` | 401 `{ error:'Code falsch oder abgelaufen.' }` | 410 `{ error:'Bitte melde dich erneut an.' }` (+ MFA-Cookie löschen) | 429.
|
||||
- `POST /api/auth/login/mfa/resend` → 200 `{ success:true }` | 410.
|
||||
|
||||
- [ ] **Step 1: Failing tests** (`test/staffLogin.test.js`):
|
||||
- `Kunde ohne MFA: unverändert` → 200, Portal-Cookie gesetzt, kein `mfaRequired`.
|
||||
- `Mitarbeiter ohne MFA: unverändert` → 200 `role:'admin'`, Portal-Cookie gesetzt.
|
||||
- `Mitarbeiter mit MFA: kein Portal-Cookie vor dem Code` → 200 `{ mfaRequired:true, email:'k…@webklar.com' }`; `set-cookie` enthält `webklar_portal_mfa`, **nicht** `webklar_portal_session` und nicht das Preview-Cookie. `GET /api/auth/me` mit diesem Cookie-Stand → `authenticated:false`.
|
||||
- `richtiger Code` → 200 `role:'admin'`, Portal-Cookie gesetzt, MFA-Cookie gelöscht; `/me` → `role:'admin'`.
|
||||
- `falscher Code` → 401 Text wörtlich; nach 5 falschen → 410 `Bitte melde dich erneut an.`.
|
||||
- `ohne MFA-Cookie` → 410.
|
||||
- `resend` → 200, `mock.mailsSent` = 2.
|
||||
- [ ] **Step 2:** `npm test` → FAIL.
|
||||
- [ ] **Step 3:** Server implementieren: `resolveStaffAccount` verschieben; im Login nach `resolveStaffAccount` bei `account?.mfa === true` → `startStaffMfa`, sonst unverändert `startStaffPortalSession`. `staff` für den späteren Erfolg wird bei `verify` neu per `getUserById` + `resolveStaffAccount` ermittelt (nicht im Speicher halten).
|
||||
- [ ] **Step 4:** `npm test` → PASS (alle, inkl. bestehender).
|
||||
- [ ] **Step 5:** Login-Seite: bei `result.mfaRequired` Passwort-Formular ausblenden, Code-Schritt zeigen (Texte aus Global Constraints, Feld `inputmode="numeric" autocomplete="one-time-code" maxlength="6"`), `Abbrechen` = Seite neu laden (`/login.html`). Erfolg → `adminLoginTarget()`.
|
||||
- [ ] **Step 6:** Sichtprüfung mit Playwright gegen den Test-Server aus Step 1 (Mock + App): Passwort → Code-Schritt sichtbar, falscher Code zeigt Text, richtiger Code landet auf `/admin.html`. Screenshot Handy- und Desktop-Breite.
|
||||
- [ ] **Step 7: Commit** `git add server/services/staffAccount.js server/routes/auth.js public/login.html public/app.js test/staffLogin.test.js && git commit -m "Portal-Login: E-Mail-Code für Mitarbeiter mit 2FA"` (+ `public/login.css`, falls geändert)
|
||||
|
||||
### Task 4: Wechsel-Link + Rückweg-Button
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/services/staffPreviewTokens.js`
|
||||
- Create: `server/routes/admin/switchLink.js`, mount in `server/index.js` als `app.use('/api/admin/switch-link', adminSwitchLinkRoutes)` neben `preview-open`
|
||||
- Modify: `server/routes/auth.js` (neue Route `GET /staff-switch`, `staff-preview` prüft `purpose`)
|
||||
- Modify: `public/admin.html` (Button in `.portal-header-actions` vor `#logout-btn`)
|
||||
- Test: `test/staffSwitch.test.js` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `resolveStaffAccount` (Task 3), `startStaffPortalSession` (Task 3).
|
||||
- Produces:
|
||||
- `createStaffPreviewToken(payload)` unverändert; `consumeStaffPreviewToken(token, purpose = 'preview')` → `null`, wenn `(entry.purpose || 'preview') !== purpose` (Token wird dabei trotzdem verbraucht).
|
||||
- `POST /api/admin/switch-link` (Bearer-JWT): `/account` per JWT (wie `requireAppwriteStaff`, aber ohne Label-Pflicht), dann `getUserById` + `resolveStaffAccount`; kein Mitarbeiter → 403; → 200 `{ openUrl: 'https://<config.preview.baseHost>/api/auth/staff-switch?token=…' }`.
|
||||
- `GET /api/auth/staff-switch?token=` → gültig: `startStaffPortalSession` + `302 /admin.html`; sonst `302 /login.html?error=Link%20abgelaufen%2C%20bitte%20anmelden`.
|
||||
- Button: `<a class="portal-btn-outline" href="https://ticket.webklar.com/tickets">← Ticketsystem</a>`.
|
||||
|
||||
- [ ] **Step 1: Failing tests:**
|
||||
- `switch-link ohne JWT` → 401; `ungültiges JWT` → 401; `JWT aus halber Sitzung` (Mock liefert `user_more_factors_required`) → 401.
|
||||
- `Kunde` (`jwt-kunde`, kein employee) → 403.
|
||||
- `Mitarbeiter ohne admin-Label` (neues Mock-JWT `jwt-staff`, employee-Dokument vorhanden) → 200 mit `openUrl`.
|
||||
- `staff-switch gültig` → 302 `/admin.html`, Portal-Cookie `role:'admin'`, `isAdmin` passend.
|
||||
- `staff-switch zweimal` → zweiter Aufruf 302 auf `/login.html?error=…`.
|
||||
- `staff-switch nach 61 s` → 302 Login.
|
||||
- `Switch-Token über staff-preview` → 410; `Preview-Token über staff-switch` → 302 Login.
|
||||
- `admin.html enthält Rückweg-Link` → statischer Test: Datei enthält `href="https://ticket.webklar.com/tickets"` innerhalb `portal-header-actions`.
|
||||
- [ ] **Step 2:** `npm test` → FAIL.
|
||||
- [ ] **Step 3:** Implementieren.
|
||||
- [ ] **Step 4:** `npm test` → PASS (alle).
|
||||
- [ ] **Step 5: Commit** `git add server/services/staffPreviewTokens.js server/routes/admin/switchLink.js server/index.js server/routes/auth.js public/admin.html test/staffSwitch.test.js && git commit -m "Wechsel Ticketsystem → Projekt-Admin per Einmal-Link, Rückweg-Button"`
|
||||
|
||||
### Task 5 (Portal): Einrichtungs-Skript `scripts/mitarbeiter-2fa.mjs`
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/mitarbeiter-2fa.mjs`, `server/services/staffMfaAdmin.js`
|
||||
- Test: `test/staffMfaAdmin.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces (`staffMfaAdmin.js`): `listStaffMfaStatus() → Promise<Array<{ userId, name, email, emailVerification, mfa }>>` (alle `employees` mit `userId`); `enableStaffMfa(userId)` (erst verification, dann mfa); `disableStaffMfa(userId)`.
|
||||
- Skript: `--status` (Tabelle), `--enable <userId>|alle` (zeigt vorher die Adressen, fragt `Ja/Nein` auf stdin; `--yes` überspringt), `--disable <userId>`. Läuft per `docker exec -w /app webklar-kundenbereich-server node scripts/mitarbeiter-2fa.mjs …`.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** `listStaffMfaStatus` gegen Mock (2 employees, 1 ohne `userId` → ignoriert); `enableStaffMfa` ruft `verification` **vor** `mfa` (Reihenfolge im Mock protokolliert); `disableStaffMfa` setzt nur `mfa:false`.
|
||||
- [ ] **Step 2:** FAIL → **Step 3:** implementieren → **Step 4:** `npm test` PASS.
|
||||
- [ ] **Step 5: Commit** `git add scripts/mitarbeiter-2fa.mjs server/services/staffMfaAdmin.js test/staffMfaAdmin.test.js && git commit -m "Skript: 2FA für Mitarbeiter anzeigen, einschalten, abschalten"`
|
||||
|
||||
## Teil B – Ticketsystem (`ticket.webklar.com/dev-2fa`)
|
||||
|
||||
### Task 6: MFA-Modul + AuthContext + Login-Seite
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/mfa.js`, `tests/mfa.test.mjs`
|
||||
- Modify: `src/lib/appwrite.js` (`export { client }`), `src/context/AuthContext.jsx`, `src/pages/LoginPage.jsx`, `package.json` (`"test": "node --test tests/"`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces (`src/lib/mfa.js`, **ohne** Import von `appwrite.js`, damit Node es laden kann):
|
||||
- `createMfaApi(client) → { createEmailChallenge(): Promise<string /*challengeId*/>, completeChallenge(challengeId, otp): Promise<void> }` – über `client.call(method, new URL(client.config.endpoint + '/account/mfa/challenges'), { 'content-type': 'application/json', 'X-Appwrite-Locale': 'de' }, params)`.
|
||||
- `isMoreFactorsError(err) → boolean` (`err?.type === 'user_more_factors_required'`)
|
||||
- `mfaErrorMessage(err) → string` (Texte aus Global Constraints; Fallback `err.message || 'Anmeldung fehlgeschlagen'`)
|
||||
- `maskEmail(email) → string` (gleiche Regel wie Portal)
|
||||
- AuthContext-Wert zusätzlich: `mfaPending: null | { challengeId: string | null, email: string }`, `verifyCode(otp) → {success, error?}`, `sendCode() → {success, error?}` (erstmals oder erneut), `cancelMfa() → Promise<void>`. `login()` liefert bei MFA `{ success:false, mfaRequired:true }`.
|
||||
|
||||
- [ ] **Step 1: Failing tests** (`tests/mfa.test.mjs`, Fake-Client zeichnet `call`-Argumente auf):
|
||||
- `createEmailChallenge` → Methode `post`, Pfad endet auf `/account/mfa/challenges`, Params `{ factor:'email' }`, Header `X-Appwrite-Locale: de`, Rückgabe = `$id`.
|
||||
- `completeChallenge('c1','123456')` → Methode `put`, Params `{ challengeId:'c1', otp:'123456' }`.
|
||||
- `isMoreFactorsError` true/false; `mfaErrorMessage` für `user_invalid_token`, Code 429, `user_email_not_verified`, sonstige.
|
||||
- `maskEmail('kenso@webklar.com') === 'k…@webklar.com'`.
|
||||
- [ ] **Step 2:** `npm test` → FAIL.
|
||||
- [ ] **Step 3:** `mfa.js` implementieren → `npm test` PASS.
|
||||
- [ ] **Step 4:** AuthContext:
|
||||
- `login()`: nach `createSessionWithCleanup` → `account.get()`; `isMoreFactorsError` → `sendCode()` + `mfaPending` setzen → `{ success:false, mfaRequired:true }`.
|
||||
- `checkUser()`: `isMoreFactorsError` → `mfaPending = { challengeId: null, email: '' }`, **keine** Mail, `user` bleibt `null`, lokale Sitzung **nicht** löschen (anders als bei anderen 401).
|
||||
- `verifyCode`: `completeChallenge` → `account.get()` → `setUser`, `ensureEmployeeExists`, `mfaPending = null`.
|
||||
- `cancelMfa`: `account.deleteSession('current')` (Fehler schlucken), `clearAllLocalAppwriteSessions()`, `mfaPending = null`.
|
||||
- E-Mail für den Hinweis: beim Login aus dem Formular; beim Reload unbekannt → Hinweis ohne Adresse `Wir schicken dir einen Code an deine E-Mail-Adresse.` bis `Code senden` gedrückt ist.
|
||||
- [ ] **Step 5:** LoginPage: bei `mfaPending` Code-Schritt statt Passwort-Formular (Texte wörtlich; `Code senden` nur wenn `challengeId === null`, sonst Codefeld + `Code erneut senden`); `/login` leitet wie bisher nur bei `user` weiter.
|
||||
- [ ] **Step 6:** UI-Harness (Scratchpad, esbuild + Playwright, Appwrite per `page.route` nachgebaut, Rezept aus Memory): Checks
|
||||
- Login ohne MFA → `/tickets`.
|
||||
- Login mit MFA → Code-Schritt, Hinweis `k…@webklar.com`, **1** Challenge-POST.
|
||||
- falscher Code → `Code falsch oder abgelaufen.`; richtiger Code → `/tickets`.
|
||||
- `Code erneut senden` → 2. Challenge-POST.
|
||||
- Reload mit halber Sitzung → Code-Schritt mit `Code senden`, **0** Challenge-POSTs, keine Weiterleitungsschleife (URL bleibt `/login`, max. 1 `GET /account`).
|
||||
- `Abbrechen` → `DELETE /account/sessions/current`, Passwort-Formular sichtbar; danach neuer Login möglich, auch wenn der Mock `DELETE /account/sessions` mit 401 beantwortet.
|
||||
- 429 beim Challenge-POST → Text `Zu viele Versuche. Bitte warte einige Minuten.`
|
||||
- [ ] **Step 7:** `npx vite build` im Worktree (unter `systemd-run --scope -p MemoryMax=700M -p MemorySwapMax=0`) → Build ok.
|
||||
- [ ] **Step 8: Commit** `git add src/lib/mfa.js tests/mfa.test.mjs src/lib/appwrite.js src/context/AuthContext.jsx src/pages/LoginPage.jsx package.json && git commit -m "Login mit E-Mail-Code (Appwrite-2FA)"`
|
||||
|
||||
### Task 7: Seitenleisten-Eintrag „Projekt-Admin“
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/Navbar.jsx`, `src/lib/employeeAdminApi.js` (oder neue kleine `src/lib/projectAdminSwitch.js` im gleichen Stil)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `POST /api/admin/switch-link` (Task 4).
|
||||
- Produces: `openProjectAdmin() → Promise<void>` – `account.createJWT()`, POST, `window.location.assign(openUrl)`; bei Fehler `window.location.assign('https://project.webklar.com/login.html')`.
|
||||
|
||||
- [ ] **Step 1:** Eintrag als `<button className="side-link">` (kein `<Link>`, externes Ziel) nach der Linkliste in derselben `side-group`, Icon `IconExternalLink`, Label `Projekt-Admin`, während des Ladens `disabled`; im eingeklappten Zustand `title` gesetzt wie die anderen.
|
||||
- [ ] **Step 2:** UI-Harness-Checks: Eintrag für Mitarbeiter ohne Admin-Rolle sichtbar; Klick → genau 1 POST auf `https://project.webklar.com/api/admin/switch-link` mit `Authorization: Bearer …`, danach Navigation auf `openUrl`; Fehlerantwort 500 → Navigation auf `…/login.html`.
|
||||
- [ ] **Step 3:** `npm test` + `vite build` ok.
|
||||
- [ ] **Step 4: Commit** `git add src/components/Navbar.jsx src/lib/employeeAdminApi.js && git commit -m "Seitenleiste: direkt ins Projekt-Admin wechseln"`
|
||||
|
||||
## Teil C – Livegang (nur mit OK des Nutzers)
|
||||
|
||||
### Task 8: Code-Mail-Vorlage + Übergabe
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/superpowers/specs/2026-09-30-mfa-mailvorlage.html` (Ticketsystem-Worktree) – Betreff `Dein Anmeldecode für WEBklar`, HTML im Stil der Portal-Vorlagen (`dev-mailvorlagen`/`server/mail-templates`), Platzhalter `{{otp}}`, Text `Der Code gilt 1 Stunde.` und `Nicht du? Dann ändere bitte dein Passwort.`
|
||||
|
||||
- [ ] **Step 1:** Vorlage erstellen, lokal mit `{{otp}} = 123456` rendern, Screenshot an den Nutzer.
|
||||
- [ ] **Step 2:** Übergabe an den Nutzer mit genauer Reihenfolge:
|
||||
1. Portal: `git -C /home/webklar/apps/kundenbereich.webklar.com/dev-2fa push origin feature/mitarbeiter-2fa:main` – danach Live-Check: `POST /api/admin/switch-link` ohne JWT → 401, Login-Seite lädt.
|
||||
2. Ticketsystem: `git -C /home/webklar/apps/ticket.webklar.com/dev-2fa push origin feature/mitarbeiter-2fa:test` – danach Live-Check Bundle enthält `Projekt-Admin`.
|
||||
3. Nutzer hinterlegt Vorlage in der Appwrite-Console (Auth → Templates → MFA-Challenge, Sprache Deutsch).
|
||||
4. `--status`, dann `--enable <Kensos userId>`, Nutzer probiert beide Logins + Wechsel, dann `--enable alle`.
|
||||
- [ ] **Step 3:** Commit der Vorlage (`git add docs/superpowers/specs/2026-09-30-mfa-mailvorlage.html`).
|
||||
@@ -0,0 +1,148 @@
|
||||
# WOMS „Rechtliches“ Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Interner Menüpunkt „Rechtliches“ mit versionierten Rechtstexten und Anbieter-Verträgen samt Fristen.
|
||||
|
||||
**Architecture:** Drei neue Appwrite-Sammlungen + ein Bucket, angelegt per idempotentem Setup-Skript. Reine
|
||||
Fristen-/Versionslogik in `src/lib/legal.js` (Node-Unit-Tests), Datenzugriff in `src/hooks/useLegal.js`,
|
||||
Oberfläche in `src/pages/LegalPage.jsx` + zwei Reiter-Komponenten. Erstbefüllung per Seed-Skript nach Freigabe
|
||||
der Versionsliste durch den Nutzer.
|
||||
|
||||
**Tech Stack:** React 18, Vite 5, Appwrite JS SDK 13 (Client) / node-appwrite (Skripte), `node --test` (Node 24), Playwright (Scratchpad).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-30-rechtliches-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Arbeitsort: Worktree `/home/webklar/apps/ticket.webklar.com/dev-rechtliches`, Branch `feature/rechtliches`. **Nie pushen** – jeder Push (egal welcher Branch) geht live. Push nur durch/nach OK des Nutzers: `git push origin feature/rechtliches:test`.
|
||||
- `node_modules` ist in Git getrackt: immer `git add <dateien>`, nie `-A`.
|
||||
- Keine neuen npm-Abhängigkeiten.
|
||||
- Oberflächentexte Deutsch. Datumsangaben als `YYYY-MM-DD`-String gespeichert, angezeigt `dd.mm.yyyy`.
|
||||
- Versionen und Dateien: kein Löschen (weder UI noch Rechte). Anbieter-Verträge: Löschen erlaubt.
|
||||
- Fristenfarben: rot ≤ 30 Tage, orange ≤ 90 Tage bis Kündigungstermin; grau = gekündigt/beendet; „Fristen fehlen“ bei unvollständigen Angaben.
|
||||
- Keine erfundenen Fristen/Preise im Seed.
|
||||
- CSS: kein Bootstrap vorhanden (`.d-flex` etc. existieren nicht), vorhandene Klassen/`ui`-Komponenten nutzen.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- Abgelöst-Setzen scheitert nach dem Anlegen der neuen Version (zweiter Request 401/Netz) → zwei „gültige“ Versionen; UI muss den Fehler zeigen und `currentVersion()` die mit dem jüngsten `validFrom` nehmen (Test Task 1).
|
||||
- Vertrag mit Start am 31. eines Monats → Monatsende klemmen, nie in den Folgemonat überlaufen (Test Task 1).
|
||||
- Kündigungstermin genau heute → zählt noch als aktuelle Periode, rot, nicht übersprungen (Test Task 1).
|
||||
- Neue Version mit `validFrom` älter als die aktuelle (Nachtrag einer alten Fassung) → darf die aktuelle NICHT ablösen, wird als `superseded` mit passendem `validTo` einsortiert (Test Task 1).
|
||||
- Bucket/Sammlung fehlt noch (Setup nicht gelaufen) → Seite zeigt leeren Zustand mit Hinweis statt Absturz (Test Task 5).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Fristen- und Versionslogik
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/legal.js`
|
||||
- Create: `tests/legal.test.mjs`
|
||||
- Modify: `package.json` (Script `"test": "node --test tests/"`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `addMonthsClamped(date: string, n: number): string`
|
||||
- `nextCancellationDate(contract, today: string): { state: 'active'|'anytime'|'ends'|'ended'|'cancelled'|'incomplete', noticeBy: string|null, periodEnd: string|null }`
|
||||
- `deadlineTone(result, today: string): 'red'|'orange'|'normal'|'grey'|'missing'`
|
||||
- `currentVersion(versions): version|null` (Status `current`, bei mehreren das jüngste `validFrom`)
|
||||
- `planNewVersion(versions, input): { create: object, updates: Array<{ id: string, patch: object }> }`
|
||||
- `formatDate(date: string): string` (`YYYY-MM-DD` → `dd.mm.yyyy`, leer → `–`)
|
||||
|
||||
- [ ] **Step 1: Failing tests** in `tests/legal.test.mjs` (`node:test`, `assert/strict`):
|
||||
- `addMonthsClamped('2026-01-31', 1) === '2026-02-28'`; `('2028-01-31', 1) === '2028-02-29'`; `('2026-03-15', 12) === '2027-03-15'`.
|
||||
- Start `2025-03-12`, term 12, renewal 12, notice 1 month, today `2026-09-30` → `{state:'active', periodEnd:'2027-03-12', noticeBy:'2027-02-12'}`.
|
||||
- gleiche Daten, today `2027-02-12` → noticeBy `2027-02-12` (heute zählt noch); today `2027-02-13` → periodEnd `2028-03-12`, noticeBy `2028-02-12`.
|
||||
- notice 30 days, periodEnd `2027-03-12` → noticeBy `2027-02-10`.
|
||||
- `termMonths: 0` → `state:'anytime'`, noticeBy null.
|
||||
- renewal 0, periodEnd in Zukunft → `state:'ends'`, periodEnd gesetzt; periodEnd vorbei → `'ended'`.
|
||||
- `cancelledAt` gesetzt → `'cancelled'`, periodEnd = `endsAt`.
|
||||
- fehlendes `startDate` oder `noticeValue` (bei term > 0) → `'incomplete'`.
|
||||
- `deadlineTone`: noticeBy in 30 Tagen → `'red'`, 31 → `'orange'`, 90 → `'orange'`, 91 → `'normal'`; cancelled/ended → `'grey'`; incomplete → `'missing'`; anytime/ends → `'normal'`.
|
||||
- `planNewVersion` mit aktueller v5 (validFrom `2026-05-01`), Eingabe v6 status `current` validFrom `2026-09-01` → updates `[{id:v5.$id, patch:{status:'superseded', validTo:'2026-08-31'}}]`, create.validTo null.
|
||||
- Eingabe status `draft` → updates leer.
|
||||
- Eingabe `current` mit validFrom `2025-01-01` (älter als v5) → create.status `'superseded'`, create.validTo = Tag vor dem nächstjüngeren validFrom (`2026-04-30`), updates leer.
|
||||
- `currentVersion` mit zwei `current` → die mit jüngerem validFrom.
|
||||
|
||||
- [ ] **Step 2:** `npm test` → FAIL (Modul fehlt).
|
||||
- [ ] **Step 3: Implementieren.** Rein, ohne Imports; Datumsrechnung in UTC auf `YYYY-MM-DD`. Algorithmus `nextCancellationDate` (active): `periodEnd = addMonthsClamped(start, term)`; solange `noticeBy(periodEnd) < today`: `periodEnd = addMonthsClamped(periodEnd, renewal)`. Notice in Monaten per `addMonthsClamped(periodEnd, -n)`, in Tagen per Tagesabzug.
|
||||
- [ ] **Step 4:** `npm test` → alle PASS.
|
||||
- [ ] **Step 5: Commit** `git add src/lib/legal.js tests/legal.test.mjs package.json && git commit -m "Rechtliches: Fristen- und Versionslogik mit Tests"`
|
||||
|
||||
### Task 2: Appwrite-Setup und Konstanten
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/setup-legal.mjs`
|
||||
- Modify: `src/lib/appwrite.js` (`Storage` exportieren, Konstanten)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `COLLECTIONS.LEGAL_DOCUMENTS = 'legalDocuments'`, `COLLECTIONS.LEGAL_VERSIONS = 'legalVersions'`, `COLLECTIONS.PROVIDER_CONTRACTS = 'providerContracts'`, `LEGAL_BUCKET_ID = 'legal-files'`, `storage` (Appwrite `Storage`).
|
||||
|
||||
- [ ] **Step 1: Skript schreiben** nach Muster `setup-woms-appwrite.mjs` (ENV `APPWRITE_API_KEY`, idempotent, vorhandene Attribute überspringen). Attribute exakt wie Spec-Abschnitt „Daten“; Datumsfelder `string(10)`, `fileIds` string-Array (size 64), `changeNote`/`note` string(5000), `status`/`costInterval`/`noticeUnit` als enum, `cost` float, Monats-/Fristzahlen integer. Rechte: `legalDocuments`/`legalVersions` = read/create/update für `users`, **kein delete**; `providerContracts` = read/create/update/delete `users`. Bucket `legal-files`: read/create `users`, kein update/delete, `maximumFileSize` 20 MB, Endungen `pdf, html, htm, png, jpg, jpeg, webp, doc, docx`. Die 7 `legalDocuments` (Keys `agb, abo-bedingungen, avv, datenschutz-website, datenschutz-portal, impressum, widerruf`, mit Titel, Live-URL, sort 1–7) als feste IDs = Key anlegen, wenn fehlend. Flag `--dry-run` gibt nur aus, was angelegt würde.
|
||||
- [ ] **Step 2:** `node --check scripts/setup-legal.mjs` → OK; `node scripts/setup-legal.mjs --dry-run` ohne Key → listet Sammlungen/Bucket/Dokumente, Exit 0.
|
||||
- [ ] **Step 3:** Konstanten in `appwrite.js`; `npm run build` → Exit 0.
|
||||
- [ ] **Step 4: Commit.** Skript wird erst in Task 6 mit Key ausgeführt.
|
||||
|
||||
### Task 3: Daten-Hook
|
||||
|
||||
**Files:**
|
||||
- Create: `src/hooks/useLegal.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 `planNewVersion`, Task 2 Konstanten.
|
||||
- Produces:
|
||||
- `useLegalDocuments(): { documents, versions, loading, error, missingSetup: boolean, addVersion(documentKey, input, files: File[]), updateVersion(id, patch), refresh }`
|
||||
- `useProviderContracts(): { contracts, loading, error, missingSetup, saveContract(data, files, id?), deleteContract(id), refresh }`
|
||||
- `fileUrl(fileId): string` (View-URL), `fileDownloadUrl(fileId): string`
|
||||
|
||||
- [ ] **Step 1: Implementieren.** Listen mit `Query.limit(5000)`. 404 auf Sammlung → `missingSetup: true`, leere Listen, kein Fehler. `addVersion`: Dateien per `storage.createFile(LEGAL_BUCKET_ID, ID.unique(), file)`, dann `planNewVersion` → erst `createDocument`, dann die `updates`; scheitert ein Update, `error` = „Neue Version gespeichert, alte Version konnte nicht abgelöst werden: …“ und trotzdem `refresh()`.
|
||||
- [ ] **Step 2:** `npm run build` → Exit 0. (Verhalten wird in Task 5 per Playwright geprüft.)
|
||||
- [ ] **Step 3: Commit.**
|
||||
|
||||
### Task 4: Seite „Rechtliches“ mit beiden Reitern
|
||||
|
||||
**Files:**
|
||||
- Create: `src/pages/LegalPage.jsx`, `src/components/legal/LegalTextsTab.jsx`, `src/components/legal/VersionForm.jsx`, `src/components/legal/ProviderContractsTab.jsx`, `src/components/legal/ContractForm.jsx`
|
||||
- Modify: `src/App.jsx` (Route `/rechtliches` in `ProtectedRoute`), `src/components/Navbar.jsx` (Eintrag „Rechtliches“ mit `IconScale` nach „Finanzen“)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 (`currentVersion`, `nextCancellationDate`, `deadlineTone`, `formatDate`), Task 3 Hooks.
|
||||
|
||||
- [ ] **Step 1: LegalTextsTab.** Pro Dokument `Card`: Titel, aktuelle Version + „gilt seit“, Link „Live-Seite“ (`target=_blank`); Entwürfe als graues `Badge` „Entwurf“. Klick klappt Versionsgeschichte auf (neu → alt nach validFrom): Nummer, `formatDate(validFrom)`–`formatDate(validTo)`, Notiz, Dateilinks (Ansehen/Herunterladen). Aktuelle Zeile grün. Button „Neue Version“ öffnet `VersionForm` (Felder: Version, Status draft/current, gültig ab, Notiz, Dateien; Pflicht: Version, gültig ab). Entwurf-Zeilen haben „Als gültig markieren“ (setzt `status current` über `planNewVersion`-Logik: Update + Ablösung). Kein Löschen-Button.
|
||||
- [ ] **Step 2: ProviderContractsTab.** Tabelle/Liste sortiert: rot, orange, normal, missing, grey; innerhalb nach noticeBy. Spalten: Anbieter, Zweck, Kosten (`12,00 €/Monat`), Laufzeit, Kündigungsfrist, nächster Kündigungstermin (bzw. „jederzeit kündbar“, „endet am …“, „gekündigt zum …“, „Fristen fehlen“). Zeilenfarbe per Klasse `legal-tone-{tone}`. Detail aufklappbar mit Notiz, Dateien, „Bearbeiten“, „Als gekündigt markieren“ (fragt `endsAt`, setzt `cancelledAt` = heute), „Löschen“ mit `confirm`. `ContractForm` für Neu/Bearbeiten.
|
||||
- [ ] **Step 3: LegalPage** mit `PageHeader` „Rechtliches“ und `Tabs` (`texte` „Unsere Rechtstexte“, `anbieter` „Verträge mit Anbietern“). Bei `missingSetup` `EmptyState` „Rechtliches ist noch nicht eingerichtet (Setup-Skript fehlt).“ CSS-Regeln für `legal-tone-*` in die bestehende globale CSS.
|
||||
- [ ] **Step 4:** `npm run build` → Exit 0.
|
||||
- [ ] **Step 5: Commit.**
|
||||
|
||||
### Task 5: Oberflächentest
|
||||
|
||||
**Files:**
|
||||
- Create (Scratchpad, nicht im Repo): `legal-e2e.mjs` – Vite-Build mit `DIST=…`, Appwrite-Mock unter `https://ticket.test` (Muster `appsmoke.mjs` aus Memory „woms-ticketsystem“), Build und Browser unter `systemd-run --scope -p MemoryMax=…`.
|
||||
|
||||
- [ ] **Step 1: Test schreiben**, Prüfungen:
|
||||
- Menü zeigt „Rechtliches“, `/rechtliches` lädt, zwei Reiter.
|
||||
- AGB-Karte zeigt aktuelle „v5“, grün; Geschichte listet v5, v4 mit `validTo`.
|
||||
- „Neue Version“ v6 current ab heute → Mock erhält Create v6 **und** Patch v5 `{status:'superseded', validTo:<gestern>}`; Karte zeigt v6.
|
||||
- Patch-Request mit 401 gemockt → Fehlermeldung sichtbar, v6 trotzdem gelistet.
|
||||
- Kein „Löschen“ bei Versionen.
|
||||
- Anbieter-Reiter: Vertrag mit noticeBy in 10 Tagen hat `legal-tone-red` und steht oben; unvollständiger zeigt „Fristen fehlen“; gekündigter ist grau und unten.
|
||||
- Sammlung liefert 404 → EmptyState-Hinweis, kein Absturz.
|
||||
- [ ] **Step 2:** Test laufen lassen → alle PASS; bei FAIL Code in Task 3/4 korrigieren und neu committen.
|
||||
|
||||
### Task 6: Einrichtung und Erstbefüllung (Nutzer-Gates)
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/seed-legal.mjs`, `scripts/legal-seed.json` (erzeugte Versionsliste)
|
||||
|
||||
- [ ] **Step 1: Versionsliste rekonstruieren.** Pro Text: `git log --format='%h %ad %s' --date=short origin/main -- <datei>` in `/home/webklar/apps/webklar.com/build` (`src/pages/AGB.tsx`, `Impressum.tsx`, `Datenschutz.tsx`) und `/home/webklar/apps/kundenbereich.webklar.com/build` (`public/abo-bedingungen.html` inkl. AVV-Anlage, `public/datenschutz.html`, `public/widerrufen.html`). Nur inhaltliche Änderungen werden Versionen (reine Format-/Build-Commits zusammenfassen). Ergebnis als `legal-seed.json`: `{documentKey, version, validFrom, status, changeNote, source: '<repo>@<hash>:<pfad>'}`. Nicht auf main liegende Stände (z. B. Abo-Bedingungen v4, AGB `f40e889`) als `draft`.
|
||||
- [ ] **Step 2: GATE – Nutzer prüft die Liste** (Nummerierung, v. a. AGB „v6“). Erst nach OK weiter.
|
||||
- [ ] **Step 3: Seed-Skript.** Liest JSON, erzeugt je Version eine Datei: Portal-HTML direkt aus `git show`; Website-TSX als lesbares HTML (Text aus JSX, eigene Mini-Umwandlung; bei Unsicherheit TSX-Quelltext als `.html` in `<pre>`). Upload in `legal-files`, Dokument in `legalVersions`. Idempotent über `source` (schon vorhanden → überspringen). Anbieter Hetzner (Server), Hetzner (Mail), Porkbun (Domains), Stripe (Zahlungen) ohne Fristen/Preise. `--dry-run`.
|
||||
- [ ] **Step 4: GATE – API-Key.** Key auf dem Server suchen (nur Dateien, die bisherige Setup-Läufe nutzten); sonst Nutzer fragen. Dann `node scripts/setup-legal.mjs`, danach `node scripts/seed-legal.mjs` → Ausgabe zählt angelegte Sammlungen/Versionen.
|
||||
- [ ] **Step 5: Commit** Skripte + JSON.
|
||||
|
||||
### Task 7: Abschluss
|
||||
|
||||
- [ ] **Step 1:** `npm test`, `npm run build`, Task-5-Test → grün. Deploy-nahe Prüfung: `git archive` + `docker run node:20-alpine` (`npm install; npm run build`).
|
||||
- [ ] **Step 2: GATE – Push-OK des Nutzers**, dann `git push origin feature/rechtliches:test`; Live-Bundle auf „Rechtliches“ prüfen.
|
||||
- [ ] **Step 3:** Memory `woms-ticketsystem` aktualisieren.
|
||||
@@ -0,0 +1,183 @@
|
||||
# Serverpuls Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Admin-only WOMS page „Serverpuls“ with server/container/app metrics over time and deterministic Low/Medium/Critical mail alerts from a small monitoring service.
|
||||
|
||||
**Architecture:** New service `/home/webklar/apps/serverpuls` (own local git repo, Node 22, `node:sqlite`, nodemailer) collects from host `/proc`, a read-only docker-socket-proxy and read-only mounts, stores downsampled series in SQLite, evaluates rules, mails via server@webklar.com and serves a read-only JWT+admin API. WOMS nginx proxies `/api/serverpuls/`; WOMS gets `ServerpulsPage` with uPlot charts.
|
||||
|
||||
**Tech Stack:** Node 22 (container) / Node 24 (host tests, `node --test`), `node:sqlite`, nodemailer, `tecnativa/docker-socket-proxy`, React 18 + Vite 5, uPlot, Playwright headless shell (host cache `chromium_headless_shell-1234`).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-30-serverpuls-design.md` (WOMS worktree `dev-serverpuls`, branch `feature/serverpuls`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- No AI anywhere in collection, rules or mails.
|
||||
- serverpuls container: `mem_limit: 128m`, `cpus: 0.5`, `init: true`, port 8091, base path `/api/serverpuls`.
|
||||
- Every API route except `/health` requires `X-Appwrite-JWT`; `/account` must return `labels` containing `admin` → else 401 (no/invalid JWT) / 403 (no admin). Auth cache 60 s.
|
||||
- API is read-only: only `GET`. No delete/stop/restart anywhere.
|
||||
- Mail: `mail.your-server.de:587` STARTTLS, user + from + to = `server@webklar.com`; password only in `/home/webklar/apps/.env` as `SERVERPULS_SMTP_PASS`, set by the user; never printed, never read by Claude.
|
||||
- Retention: raw 5 min for 7 days → hourly (avg + max) for 90 days → daily forever. Sizes daily forever.
|
||||
- Schedules (Europe/Berlin, via `Intl`): sample every 5 min; sizes 03:00; downsampling 03:30; Medium digest daily 07:00; Low weekly Monday 07:00. Due-state stored in SQLite `meta` so restarts neither skip nor double-send.
|
||||
- Missing measurement = no row (gap), never 0.
|
||||
- Critical containers: `webklar-traefik`, `webklar-appwrite`, `webklar-kundenbereich-server`, `webklar-ticket`, `webklar-ticket-integrations`, `webklar-gitea`, `webklar-invoiceninja`, `webklar-n8n`.
|
||||
- Mail subjects start with `[Serverpuls]`; texts German.
|
||||
- WOMS: never push; never `git add -A` (node_modules is tracked); branch `feature/serverpuls`. Live = the user pushes to `test`.
|
||||
- Heavy work (vite build, Playwright, npm install) runs under `systemd-run --scope -p MemoryMax=…M -p MemorySwapMax=0` (build 700M, browser 700M) — swap is full.
|
||||
- Before editing `/home/webklar/apps/docker-compose.yml`, `.env`-touching scripts or the WOMS nginx conf: backup `*.bak-2026-09-30-serverpuls`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- Small apps flapping in growth rules (1 MB → 2 MB = +100 %): percentage growth rules only apply when the app is ≥ 100 MB at the later point (absolute 2 GB rule always applies). Test in Task 3.
|
||||
- First days without 7-day history: growth rules silent until a size row ≥ 6 days old exists (no division by missing/zero). Test in Task 3.
|
||||
- Service restart / downtime around 07:00: digest is sent once, on the first tick after 07:00 the same day; never twice. Test in Task 4.
|
||||
- Container that is deliberately stopped (e.g. Paperless, exit code 0): no alert; only non-zero exit or restart loop counts for non-critical. Test in Task 3.
|
||||
- API polled by the page every minute while Appwrite is slow: auth cache prevents one `/account` call per request; Appwrite down → 502 with JSON `{error}`, page shows the error instead of blank charts. Tests in Task 5 and Task 8.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
Service repo `/home/webklar/apps/serverpuls/`:
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `Dockerfile` | `FROM node:22-alpine`, `apk add --no-cache util-linux-misc` (ionice), `npm ci --omit=dev`, `CMD node src/main.js` |
|
||||
| `package.json` | `"type":"module"`, dep `nodemailer`, script `test: node --test test/` |
|
||||
| `config/apps.json` | container → app grouping, app → folders |
|
||||
| `config/rules.json` | thresholds + critical container list |
|
||||
| `src/collect/host.js` | parse `/proc` + statfs |
|
||||
| `src/collect/docker.js` | proxy client: list, inspect, stats, system df |
|
||||
| `src/collect/sizes.js` | nightly `du` per app + previews + cleanup numbers |
|
||||
| `src/apps.js` | map containers/volumes/folders to apps |
|
||||
| `src/store.js` | SQLite schema, inserts, queries, downsampling, meta |
|
||||
| `src/rules.js` | pure rule evaluation |
|
||||
| `src/alerts.js` | state diff, mail decisions, digests |
|
||||
| `src/mail.js` | nodemailer + queue |
|
||||
| `src/api.js` | HTTP API + auth |
|
||||
| `src/schedule.js` | Berlin-time due checks |
|
||||
| `src/main.js` | wiring + timers |
|
||||
| `test/*.test.js`, `test/fixtures/` | unit/API tests |
|
||||
|
||||
WOMS worktree `dev-serverpuls`: `src/lib/serverpulsApi.js`, `src/lib/serverpulsFormat.js`, `src/components/serverpuls/{Chart.jsx,Sparkline.jsx,AppsTable.jsx}`, `src/pages/ServerpulsPage.jsx`, `src/styles/serverpuls.css`; modify `src/App.jsx`, `src/components/Navbar.jsx`, `package.json`, `package-lock.json`; test `tests/unit/serverpulsFormat.test.mjs`.
|
||||
|
||||
Ops: `/home/webklar/apps/docker-compose.yml`, `/home/webklar/apps/ticket.webklar.com/nginx/ticket.webklar.com.conf`, `/home/webklar/apps/scripts/mail-zugang.py`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Repo, host collector, app mapping
|
||||
|
||||
**Files:**
|
||||
- Create: `Dockerfile`, `package.json`, `.gitignore` (`node_modules`, `data`), `config/apps.json`, `src/collect/host.js`, `src/apps.js`
|
||||
- Test: `test/host.test.js`, `test/apps.test.js`, fixtures `test/fixtures/proc/{stat,stat2,meminfo,loadavg}` and `test/fixtures/proc/{101,102,103}/stat` (103 in state `Z`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `readHost(procRoot: string, diskPath: string, prevCpu?: {idle,total}) → Promise<{cpuPct: number|null, cpuRaw:{idle,total}, load1, load5, load15, memAvail, memTotal, swapUsed, swapTotal, diskUsed, diskTotal, zombies}>` (bytes; `cpuPct` null when no `prevCpu`).
|
||||
- Produces: `loadAppsConfig(path) → AppsConfig`; `appForContainer(cfg, {name, service, project}) → string`; `foldersForApp(cfg, app) → string[]` (relative to apps root).
|
||||
|
||||
- [ ] **Step 1:** `git init` in `/home/webklar/apps/serverpuls`; package.json, Dockerfile, .gitignore. Install nodemailer + lockfile in a throwaway `docker run --rm -v $PWD:/w -w /w node:22-alpine npm install nodemailer` under MemoryMax 700M.
|
||||
- [ ] **Step 2: Failing tests** — `host.test.js`: with fixtures `stat`→`stat2` delta, `cpuPct` equals the hand-computed value (write it into the fixture comment); `memAvail` = MemAvailable×1024; `swapUsed` = (SwapTotal−SwapFree)×1024; `zombies === 1`; unreadable `/proc/<pid>/stat` (pid dir without stat) is skipped, not thrown. `apps.test.js`: `webklar-appwrite-worker-mails` → `Appwrite`; `openruntimes-executor` → `Appwrite`; `webklar-nextcloud25-db` → `Nextcloud 25`; `webklar-ticket`, `webklar-ticket-build`, `webklar-ticket-integrations` → `Ticketsystem`; `debian-app-1` (project `debian`) → `debian`; unknown `webklar-foo` → `foo`.
|
||||
- [ ] **Step 3:** Run `node --test test/` → FAIL.
|
||||
- [ ] **Step 4:** Implement. `config/apps.json` shape: `{"groups":[{"app":"Appwrite","match":["webklar-appwrite*","openruntimes-executor"],"folders":["appwrite"]}, …]}`, glob `*` only; fallback = container name minus `webklar-` prefix, or compose project for non-`apps` projects. Seed groups for Appwrite, Kundenportal (`webklar-kundenbereich-*`, folder `kundenbereich.webklar.com`), Ticketsystem (`ticket.webklar.com`, `ticket-integrations`), Website (`webklar-webclaw*`, `webklar.com`), Nextcloud, Nextcloud 25, InvoiceNinja (`InvoiceNinja`), Gitea, n8n, Bitwarden, E-Mail-Sorter (`emailsorter*`), eship, Previews-System (`webklar-preview-*`, `previews`), Traefik. Disk via `fs.statfsSync(diskPath)`.
|
||||
- [ ] **Step 5:** `node --test test/` → PASS. Commit `serverpuls: host collector + app mapping`.
|
||||
|
||||
### Task 2: Store and downsampling
|
||||
|
||||
**Files:** Create `src/store.js`; Test `test/store.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `openStore(file: string) → Store` with
|
||||
`insertHost(ts, host)`, `insertApps(ts, [{app, cpuPct, memBytes}])`, `insertContainers(ts, [{name, app, status, health, exitCode, restartCount}])`, `insertSizes(day 'YYYY-MM-DD', [{app, bytes}])`, `insertCleanup(day, {imagesBytes, buildCacheBytes, stoppedBytes, stopped:[{name,bytes}]})`,
|
||||
`hostSeries(metric, sinceTs, res: 'raw'|'hour'|'day') → [{ts, v, max}]`, `appSeries(app, field: 'cpu'|'mem', sinceTs, res)`, `sizeSeries(app|null, sinceDay) → [{day, app, bytes}]`, `containerHistory(name, sinceTs)`, `latestHost()`, `latestContainers()`, `latestCleanup()`,
|
||||
`downsample(nowTs)`, `getMeta(key)`, `setMeta(key, value)`, `alertState() → Map`, `saveAlertState(Map)`, `queueMail(msg)`, `pendingMails()`, `markMailSent(id)`, `dropMailsOlderThan(ts)`.
|
||||
- Metrics names: `cpu, load1, load5, load15, memAvail, memTotal, swapUsed, swapTotal, diskUsed, diskTotal, zombies`.
|
||||
|
||||
- [ ] **Step 1: Failing tests** (`:memory:` DB): insert 24 raw rows 5 min apart at 10 days ago → `downsample(now)` leaves 0 raw, 2 hour rows with correct avg and max; hour rows older than 90 days become day rows; raw from 1 day ago untouched; `containerHistory` rows older than 7 days deleted; `setMeta/getMeta` round-trip; alert state round-trip survives reopen of a temp file DB.
|
||||
- [ ] **Step 2:** Run → FAIL. **Step 3:** Implement (tables per spec; `host_samples(ts, res, metric…, *_max)` one row per ts; indexes on `(res, ts)`; `app_samples(ts,res,app,cpu,cpu_max,mem,mem_max)`). **Step 4:** Run → PASS. **Step 5:** Commit `serverpuls: sqlite store + downsampling`.
|
||||
|
||||
### Task 3: Rules
|
||||
|
||||
**Files:** Create `config/rules.json`, `src/rules.js`; Test `test/rules.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Store queries (Task 2) via an input object built in `main.js`.
|
||||
- Produces: `evaluate(input, rules) → Finding[]` where
|
||||
`input = {now, hostRaw:[{ts, …metrics}] (last 2 h), containers:[{name, app, status, health, exitCode, restartCount}], restarts1h: {name: n}, sizes:[{day, app, bytes}] (last 8 days), cleanup, zombieSince: ts|null, critical: string[]}` and
|
||||
`Finding = {id: string, level: 'low'|'medium'|'critical', title: string, value: string}`.
|
||||
- Ids: `disk`, `mem`, `load`, `zombies`, `zombies-persist`, `container:<name>`, `growth:<app>`, `cleanup`.
|
||||
|
||||
- [ ] **Step 1: Failing tests** — one per spec rule and both sides of each threshold: disk 89.9 % → medium, 90 % → critical; mem critical needs every sample in last 10 min to have memAvail < 500 MB **and** swap ≥ 90 % (and ≥ 2 samples); one good sample inside window → not critical; swap ≥ 75 % for 1 h → medium; load5 > 24 for 15 min → critical, > 8 for 1 h → medium; zombies 100 → critical, 20 → medium, ≥ 1 for > 24 h → low `zombies-persist`; critical container `exited` → critical; any container restartCount +4 in 1 h → critical if in list else medium; non-critical exited with exitCode 0 → none, exitCode 1 → medium; growth 7 d +21 % on 500 MB app → medium; +21 % on 50 MB app → none; +2.1 GB on 50 GB app → medium; +11 % week on 500 MB → low; only 3 days of sizes → no growth finding; images 11 GB → low `cleanup`. Each finding has non-empty German `title` and `value`.
|
||||
- [ ] **Step 2:** Run → FAIL. **Step 3:** Implement with values from `config/rules.json` (copy the spec numbers verbatim; `minGrowthPctBaseBytes: 104857600`). One finding per id, highest level wins. **Step 4:** Run → PASS. **Step 5:** Commit `serverpuls: warning rules`.
|
||||
|
||||
### Task 4: Alerts, schedule, mail
|
||||
|
||||
**Files:** Create `src/schedule.js`, `src/alerts.js`, `src/mail.js`; Test `test/alerts.test.js`, `test/schedule.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `berlinParts(ts) → {day:'YYYY-MM-DD', hour, minute, weekday: 1..7}`; `isDue(kind: 'sizes'|'downsample'|'daily'|'weekly', ts, lastRunDay) → boolean` (03:00 / 03:30 / 07:00 / Monday 07:00; due once per day/week after the time).
|
||||
- Produces: `processFindings(findings, state: Map, now) → {state, immediate: Mail[], resolvedForDigest: {medium:[], low:[]}}` — state entry `{id, level, since, lastMailed}`; critical new or re-mail after 24 h or escalation → immediate; critical resolved → immediate „✅ behoben“; medium/low resolved → collected for next digest.
|
||||
- Produces: `dailyDigest(state, resolved, now) → Mail|null` (null if nothing active or resolved), `weeklyDigest(state, resolved, tops, now) → Mail` (always, contains the three Top-5 lists); `Mail = {subject, text, html}`.
|
||||
- Produces: `createMailer(env) → {send(mail) → Promise}`; `flushQueue(store, mailer, now)` retries every 5 min, drops after 24 h; empty `SERVERPULS_SMTP_PASS` → mails stay queued, one log line, no crash.
|
||||
|
||||
- [ ] **Step 1: Failing tests** — new critical → 1 immediate, subject `[Serverpuls] 🔴 Critical: <title>`; same finding again 1 h later → 0; 24 h later → 1; medium → critical escalation → 1 immediate; critical disappears → `[Serverpuls] ✅ behoben: <title>`; medium only appears in `dailyDigest` subject `[Serverpuls] 🟠 Tagesbericht: n Warnungen`; weekly subject `[Serverpuls] 🟡 Wochenübersicht KW <n>`; `isDue('daily')` at 06:59 Berlin false, 07:00 true, again same day with lastRunDay=today false, service down 06:00–09:00 then tick 09:05 → true once; DST switch day (2026-10-25) still exactly once. `flushQueue` with a failing mailer keeps the mail; with success marks it sent; mailer without password never calls SMTP and keeps the mail.
|
||||
- [ ] **Step 2:** Run → FAIL. **Step 3:** Implement; nodemailer `{host, port:587, secure:false, requireTLS:true, auth}` from `SERVERPULS_SMTP_*`; mails text + simple HTML table, link `https://ticket.webklar.com/serverpuls`. **Step 4:** Run → PASS. **Step 5:** Commit `serverpuls: alerts, digests, mail queue`.
|
||||
|
||||
### Task 5: API
|
||||
|
||||
**Files:** Create `src/api.js`; Test `test/api.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Store (Task 2), active alert state.
|
||||
- Produces: `createApi({store, getAlerts: () => Finding-state[], appwrite: {endpoint, projectId}, fetchImpl}) → http.Server` with routes from the spec table. Range → resolution: `24h`,`7d` raw; `30d`,`90d` hour; `1y` day. `/overview` returns `{lastSample, host: {metric: {value, spark24h:[…], deltaYesterday}}, alerts:[…], level: 'ok'|'medium'|'critical'}`; `/apps` → `{sizesDay, rows}` (sizesDay = date of the latest size measurement, shown on the page), rows `{id, name, cpuPct, memBytes, sizeBytes, delta7d, delta30d, spark30d}` (deltas null without history); `/apps/:id` `{size, mem, cpu}` series; `/cleanup` = latest cleanup row.
|
||||
|
||||
- [ ] **Step 1: Failing tests** (seeded `:memory:` store, fake `fetchImpl` for `/account`): no JWT → 401; JWT with `labels:[]` → 403; admin → 200 on every route; `/host?metric=bogus` → 400; `/host?range=30d` returns hour rows; `/apps/unknown` → 404; POST → 405; `/account` throws → 502 `{error}`; two requests with same JWT within 60 s → one `/account` call; `/health` without JWT → 200.
|
||||
- [ ] **Step 2:** Run → FAIL. **Step 3:** Implement (plain `node:http`, pattern of `ticket-integrations/server.js`). **Step 4:** Run → PASS. **Step 5:** Commit `serverpuls: read-only admin API`.
|
||||
|
||||
### Task 6: Docker + sizes collectors, main wiring
|
||||
|
||||
**Files:** Create `src/collect/docker.js`, `src/collect/sizes.js`, `src/main.js`; Test `test/docker.test.js`, `test/sizes.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `dockerClient(baseUrl) → {list(), inspect(id), stats(id), systemDf()}`; `containerCpuPct(stats) → number|null` (standard `cpu_delta/system_delta*online_cpus*100`); `memBytes(stats)` = usage − inactive_file (fallback cache).
|
||||
- Produces: `measureSizes({appsRoot, volumesRoot, previewsRoot, apps: [{app, folders, volumes}], timeoutMs: 1800000}) → Promise<[{app, bytes}]>` using `ionice -c3 nice -n 19 du -sk` (fallback `nice` only if ionice missing); each preview folder → app `Preview: <name>`.
|
||||
- `main.js`: env `DB_FILE=/data/serverpuls.db`, `PROC_ROOT=/host/proc`, `DISK_PATH=/host/apps`, `APPS_ROOT=/host/apps`, `VOLUMES_ROOT=/host/volumes`, `PREVIEWS_ROOT=/host/previews`, `DOCKER_URL=http://serverpuls-docker-proxy:2375`; 60 s tick: sample if ≥ 5 min since last; due jobs; rules → alerts → queue → flush. Every collector failure is caught and logged; others still stored.
|
||||
|
||||
- [ ] **Step 1: Failing tests** — `containerCpuPct` on a fixture stats JSON equals hand value; missing `precpu_stats` → null; `measureSizes` on a temp tree (folder A 3 files, volume dir, previews/x) returns the three apps with sizes ≥ file bytes; a `du` that exceeds `timeoutMs` (set 1) rejects/returns partial without throwing out of `measureSizes`.
|
||||
- [ ] **Step 2:** Run → FAIL. **Step 3:** Implement. **Step 4:** Run all `node --test test/` → PASS. **Step 5:** Commit `serverpuls: docker/sizes collectors + main loop`.
|
||||
|
||||
### Task 7: Deploy service (ops, with user gate for mail)
|
||||
|
||||
**Files:** Modify `/home/webklar/apps/docker-compose.yml` (backup first), `/home/webklar/apps/scripts/mail-zugang.py` (backup first), `/home/webklar/apps/ticket.webklar.com/nginx/ticket.webklar.com.conf` (backup first).
|
||||
|
||||
- [ ] **Step 1:** Compose: add `serverpuls-docker-proxy` (`tecnativa/docker-socket-proxy`, `CONTAINERS=1 IMAGES=1 SYSTEM=1 POST=0`, socket `:ro`, `mem_limit: 32m`, `webklar-network`, no Traefik labels) and `serverpuls` (`build: ./serverpuls`, `container_name: webklar-serverpuls`, limits from Global Constraints, mounts `/proc:/host/proc:ro`, `./:/host/apps:ro`, `/var/lib/docker/volumes:/host/volumes:ro`, `/var/www/previews:/host/previews:ro`, `serverpuls-data:/data`, env `SERVERPULS_SMTP_USER=server@webklar.com`, `SERVERPULS_SMTP_PASS=${SERVERPULS_SMTP_PASS:-}`, `TZ=Europe/Berlin`). `docker compose config -q` → exit 0. Note: mount `/host/previews` is an addition to the spec's mount list (needed for preview apps).
|
||||
- [ ] **Step 2:** `docker compose up -d --build serverpuls-docker-proxy serverpuls` (build under MemoryMax 700M). Check: `docker stats --no-stream webklar-serverpuls` < 128 MiB; after 6 min `wget -qO- http://webklar-serverpuls:8091/api/serverpuls/health` from `webklar-ticket-integrations` shows `lastSample`; proxy rejects `POST /containers/x/stop` with 403.
|
||||
- [ ] **Step 3:** nginx: add `location /api/serverpuls/` block mirroring `/api/integrations/` (`set $serverpuls_upstream webklar-serverpuls; proxy_pass http://$serverpuls_upstream:8091;`, no `client_max_body_size`). `docker exec webklar-ticket nginx -t && docker exec webklar-ticket nginx -s reload`. `curl -s -o /dev/null -w '%{http_code}' https://ticket.webklar.com/api/serverpuls/overview` → 401.
|
||||
- [ ] **Step 4:** `mail-zugang.py --serverpuls`: prompt password for server@webklar.com (getpass), test login, backup + write `SERVERPULS_SMTP_PASS` into `.env`, `docker compose up -d serverpuls`, send test mail `[Serverpuls] Testmail` from inside the container. Dry-run with `WEBKLAR_ROOT` copy + fake SMTP. **User gate:** the user runs it; Claude only checks for the `250` result line.
|
||||
- [ ] **Step 5:** Commit service repo; record compose/nginx/script changes in the memory file.
|
||||
|
||||
### Task 8: WOMS page
|
||||
|
||||
**Files:** Create `src/lib/serverpulsApi.js`, `src/lib/serverpulsFormat.js`, `src/components/serverpuls/{Chart.jsx,Sparkline.jsx,AppsTable.jsx}`, `src/pages/ServerpulsPage.jsx`, `src/styles/serverpuls.css`; Modify `src/App.jsx` (route `/serverpuls`), `src/components/Navbar.jsx` (entry after „Rechtliches“, before „Admin“, only `isAdmin`, pulse icon, dot from `overview.level`, fetched on app load and every 5 min, admins only), `package.json`/`package-lock.json` (`uplot`); Test `tests/unit/serverpulsFormat.test.mjs`.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: API from Task 5.
|
||||
- Produces: `serverpulsApi.{overview(), host(metric, range), apps(), app(id, range), cleanup()}` (JWT pattern of `integrationsApi.js`, base `/api/serverpuls`); `formatBytes(n) → '1,2 GB'` (de-DE, base 1024), `formatDelta(pct|null) → {text:'+12,3 %'|'–', dir:'up'|'down'|'flat'}`, `ageLabel(ts, now) → {text:'vor 4 Min.', stale: boolean}` (stale > 15 min).
|
||||
|
||||
- [ ] **Step 1: Failing test** `serverpulsFormat.test.mjs`: `formatBytes(1288490189)` → `'1,2 GB'`; `formatBytes(0)` → `'0 B'`; `formatDelta(null).text` → `'–'`; `formatDelta(-3.21)` → `{text:'−3,2 %', dir:'down'}`; `ageLabel` at 16 min → stale true, 14 min → false.
|
||||
- [ ] **Step 2:** `npm test` → FAIL. **Step 3:** Implement format helpers → PASS.
|
||||
- [ ] **Step 4:** `uplot` into package.json + lockfile via throwaway `docker run node:20-alpine` (never touch tracked node_modules). Implement page per spec „WOMS-Seite“ (ampel, 6 tiles, big chart with range + metric selector, sortable/searchable apps table with row → detail chart, „Aufräumbar“, 60 s refresh while `document.visibilityState==='visible'`, error box on API error, non-admin → `<Navigate to="/tickets">`). Mobile: tiles stack < 700 px.
|
||||
- [ ] **Step 5:** Deploy-like build: `git archive HEAD` + `docker run --rm node:20-alpine sh -c 'npm install && npm run build'` under MemoryMax 700M → exit 0. Commit `Serverpuls: Admin-Seite mit Verlauf und Warnungen` (only the listed files).
|
||||
|
||||
### Task 9: UI test
|
||||
|
||||
**Files:** Create (scratchpad, not in repo) `serverpuls-e2e.mjs`.
|
||||
|
||||
- [ ] **Step 1:** Vite build with `DIST=…` (MemoryMax 700M); serve via Playwright `page.route` under `https://ticket.test`, mock Appwrite (`/v1/account` with/without `labels:['admin']`) and `/api/serverpuls/*` fixtures (one medium alert, 3 apps, one with `delta7d:null`).
|
||||
- [ ] **Step 2:** Assertions: admin sees nav entry „Serverpuls“ with orange dot; non-admin sees no entry and `/serverpuls` redirects to `/tickets`; ampel text contains „1 Warnung“; 6 tiles; switching range to „30 T“ requests `range=30d`; clicking column „Δ 7 T“ sorts; null delta shows „–“; row click opens detail chart; `/overview` 502 → error box visible, no crash; `lastSample` 20 min old → stale hint; width 390 px → no horizontal scroll.
|
||||
- [ ] **Step 3:** Run under MemoryMax 700M → all pass; screenshot desktop + mobile for the user.
|
||||
|
||||
### Task 10: Finish
|
||||
|
||||
- [ ] **Step 1:** Service: `node --test test/` all pass; live container RAM < 128 MiB after 1 h; first rows in `/overview`.
|
||||
- [ ] **Step 2:** WOMS: `npm test` all pass; final whole-branch review.
|
||||
- [ ] **Step 3:** Report to the user: what is live (service + nginx), what waits (WOMS push `git -C dev-serverpuls push origin feature/serverpuls:test`), password step status. Update memory.
|
||||
@@ -0,0 +1,256 @@
|
||||
# Unterstützung bei Arbeitsblättern + 15-Minuten-Zeiten – Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Mitarbeiter können auf Arbeitsblättern als Unterstützung (Person + Tätigkeit) eingetragen werden und bekommen 50 % der Arbeitszeit (aufgerundet auf 15 min) angerechnet; Zeiten gibt es überall nur noch in 15-Minuten-Schritten; Kundenportal, Stundenkonto und Verträge ziehen mit.
|
||||
|
||||
**Architecture:** Neues JSON-Attribut `supporters` auf `worksheets`; Minuten werden nie gespeichert, sondern mit derselben Rechenregel in WOMS (`src/lib/support.js`) und Portal (`server/services/worksheetMinutes.js`) aus `totalTime` berechnet. WOMS bekommt eine gemeinsame Zeitauswahl (`DurationSelect`) und zwei Wege, Unterstützung einzutragen (Ersteller im Zeiten-Formular, Helfer über „Ich habe unterstützt“). Das Portal zeigt Unterstützung als eigene Zeilen, eine Gesamtzeit oben und zieht sie im Abo-Stundenkonto ab. Abo-Bedingungen Fassung 5 und AGB § 9 Abs. 2 halten die Regeln fest.
|
||||
|
||||
**Tech Stack:** WOMS React/Vite + Appwrite Web SDK 13, Tests `node --test` (+ esbuild `tests/unit/load.mjs`), Playwright-Harness; Portal Node/Express ESM, `node --test test/*.test.js`, Vanilla-JS `public/app.js`; webklar.com React/TS + vitest.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-30-unterstuetzung-design.md` (WOMS-Repo, Commit a336dc0)
|
||||
|
||||
## Repos und Arbeitsverzeichnisse
|
||||
|
||||
| Teil | Repo / Zweig | Worktree | Livegang |
|
||||
|---|---|---|---|
|
||||
| WOMS (Tasks 1–6) | JUSN/tickte-system, `feature/unterstuetzung` von `origin/test` | `/home/webklar/apps/ticket.webklar.com/dev-unterstuetzung` (existiert) | `git push origin feature/unterstuetzung:test` |
|
||||
| Portal (Tasks 7–10) | Portal-Repo, `feature/unterstuetzung` von `origin/main` | `/home/webklar/apps/kundenbereich.webklar.com/dev-unterstuetzung` (in Task 7 anlegen) | `git push origin feature/unterstuetzung:main` |
|
||||
| webklar.com (Task 11) | JUSN/Webklar, `fix/agb-haftung` (f40e889) | `/home/webklar/apps/webklar.com/dev-agb-haftung` (existiert, erst auf `origin/main` rebasen) | `git push origin fix/agb-haftung:main` |
|
||||
|
||||
Worktree-Regeln (alle drei Repos): `node_modules` ist ein Symlink auf `../build/node_modules`; nach Rebase/Checkout `rm -rf node_modules && ln -s ../build/node_modules node_modules`. In WOMS ist `node_modules` getrackt → nur `git add <dateien>`, nie `-A`. Nichts pushen ohne OK des Nutzers; der WOMS-Webhook deployt JEDEN gepushten Zweig.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Zeit-Raster: `TIME_STEP = 15` Minuten; Auswahl 15 min … 12 h (`MAX_STEP_MINUTES = 720`, 48 Werte; 720 = bestehendes `MAX_MINUTES` der Push-Zeitmail). *Abweichung von der Spec (dort „bis 10 h“) zugunsten eines Maximums in beiden Systemen – Spec-Zeile wird in Task 1 angepasst.*
|
||||
- Berechnete Dauer wird **aufgerundet** auf das nächste Vielfache von 15; Dauer 0/fehlend → 15 vorausgewählt.
|
||||
- `supportMinutes(t) = t > 0 ? ceil(t / 2 / 15) * 15 : 0`; Testvektoren überall: 15→15, 30→15, 45→30, 60→30, 75→45, 70→45, 0→0, -5→0, '60'→30, null→0.
|
||||
- `worksheetMinutes(ws) = base + supporters.length * supportMinutes(base)`, `base = Number(ws.totalTime) || 0`; Kommentar-Filter bleibt beim Aufrufer.
|
||||
- `supporters`-Eintrag: `{ employeeId: string, name: string, task: string }`; `task` getrimmt 1–200 Zeichen; `employeeId` eindeutig; gespeichert als JSON-String, Attribut `supporters` String 4000, nicht Pflicht.
|
||||
- Alte Arbeitsblätter werden nicht umgerechnet.
|
||||
- Anzeige-Texte (exakt): Haken „Unterstützung“, Feld „Wobei?“, Knopf „+ weitere Person“, Knopf „Ich habe unterstützt“ / „Meine Unterstützung bearbeiten“, Listenzeile WOMS „↳ Unterstützung {Vorname} · {task} · {Zeit}“, Portal „↳ Unterstützung {Vorname}: {task}“ + Zeit, Portal-Kopf „Arbeitszeit gesamt: {Zeit}“, Stundenkonto-Zusatz „inkl. {Zeit} Unterstützung“.
|
||||
- Vorname = erstes Wort von `name`.
|
||||
- Zeitformat WOMS wie bisher `formatTime` („1h 30min“); Portal wie bisher `fmtMinutes`/`fmtHours`.
|
||||
- Berechtigungen nur in der Oberfläche; Appwrite-Rechte bleiben unverändert.
|
||||
- Claude darf keinen Appwrite-API-Key benutzen: Skripte mit Key führt der Nutzer aus.
|
||||
- Commits enden mit `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. Ersteller speichert das Zeiten-Formular, während ein Kollege sich gerade selbst als Unterstützung eingetragen hat → dessen Eintrag darf nicht still verloren gehen (Task 4: Konflikt-Prüfung beim Speichern).
|
||||
2. Altes Arbeitsblatt mit 70 min wird zum Bearbeiten geöffnet → 75 ist vorausgewählt, und Unterstützung rechnet dann mit 75 (Task 3 + Task 1-Vektoren).
|
||||
3. Git-Push: der zugeordnete Pusher sieht „Zeiten bearbeiten“ statt „Ich habe unterstützt“; ein Push ohne nachgetragene Zeit bietet keinem „Ich habe unterstützt“ an (Task 1 `canAddOwnSupport`, Task 5).
|
||||
4. `supporters` kaputt (kein JSON, kein Array, Eintrag ohne `task`, doppelte Person) → WOMS-Liste, Portal-Projektansicht und Stundenkonto laufen weiter und ignorieren nur den kaputten Teil (Task 1, Task 7).
|
||||
5. Mitarbeiterliste lädt nicht oder ein Mitarbeiter hat kein `userId` → Auswahl zeigt Hinweis statt leerer Zeilen, „Ich habe unterstützt“ funktioniert trotzdem mit `user.$id`/`user.name` (Task 4, Task 5).
|
||||
|
||||
---
|
||||
|
||||
## WOMS
|
||||
|
||||
### Task 1: Rechenregeln (Zeit-Raster + Unterstützung)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/timeSteps.js`, `src/lib/support.js`
|
||||
- Test: `tests/unit/support.test.mjs` (lädt Module über `tests/unit/load.mjs`)
|
||||
- Modify: `docs/superpowers/specs/2026-09-30-unterstuetzung-design.md` („bis 10 h (40 Werte)“ → „bis 12 h (48 Werte)“)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces (`timeSteps.js`): `TIME_STEP = 15`, `MAX_STEP_MINUTES = 720`, `roundUpToStep(minutes: number|null): number` (≤0/NaN/null → 15, sonst `ceil(m/15)*15`), `isValidStep(minutes: any): boolean` (ganze Zahl, >0, %15===0, ≤720), `stepOptions(current?: number): {value:number,label:string}[]` (15…720; liegt `current` gültig über 720, wird er angehängt), `stepLabel(minutes): string` („15 min“, „1 h“, „1 h 15 min“).
|
||||
- Produces (`support.js`): `supportMinutes(total): number`, `parseSupporters(raw: string|array|null): Supporter[]` (nie Exception; verwirft Einträge ohne `employeeId`/`task`, trimmt, kürzt `task` auf 200, erster Eintrag je `employeeId` gewinnt), `serializeSupporters(list): string`, `worksheetMinutes(ws): number`, `firstName(name): string`, `canAddOwnSupport(ws, user, employee): boolean` (`user.$id` vorhanden, `!ws.isComment`, `Number(ws.totalTime) > 0`, `!isWorksheetCreator(ws, user, employee)`), `upsertSupporter(list, entry): Supporter[]`, `removeSupporter(list, employeeId): Supporter[]`, `validateSupporters(list, creatorUserId): string|null` (Fehlertext: „Bitte bei jeder Person angeben, wobei sie unterstützt hat.“, „Jede Person nur einmal eintragen.“, „Du kannst dich nicht selbst als Unterstützung eintragen.“).
|
||||
- Consumes: `isWorksheetCreator` aus `src/lib/ticketForm.js`.
|
||||
|
||||
- [ ] **Step 1: Failing tests** in `tests/unit/support.test.mjs`: Vektoren aus Global Constraints für `supportMinutes`; `roundUpToStep`: 0→15, 50→60, 60→60, 61→75, null→15; `isValidStep`: 15/720 true, 70/0/735/'x'/7.5 false; `stepOptions()` Länge 48, erstes `{15,'15 min'}`, `[3]` = `{60,'1 h'}`, `[4]` = `{75,'1 h 15 min'}`; `stepOptions(900)` Länge 49; `parseSupporters`: `'kaputt'`→[], `'{}'`→[], `'[{"employeeId":"a","name":"Andrej S","task":" Farbe "},{"employeeId":"a","name":"x","task":"y"},{"employeeId":"b","name":"Nico"}]'` → `[{employeeId:'a',name:'Andrej S',task:'Farbe'}]`; `worksheetMinutes({totalTime:60, supporters:<2 Einträge>})` = 120, `{totalTime:70, supporters:<1>}` = 115, ohne `supporters` = `totalTime`; `canAddOwnSupport`: fremdes normales Blatt mit Zeit → true, eigenes → false, Kommentar → false, Git ohne Zeit (`isComment:true,totalTime:0`) → false, Git mit Zeit gepusht von `knso` für `employee.shortcode 'KNSO'` → false, für anderen Mitarbeiter → true; `validateSupporters` für die drei Fehler und `null` für gültige Liste; `firstName('Andrej Stevanovski')` = 'Andrej'.
|
||||
- [ ] **Step 2:** `npm test` → neue Tests FAIL (Modul fehlt).
|
||||
- [ ] **Step 3:** `timeSteps.js` und `support.js` mit den Signaturen oben implementieren (reine Funktionen, keine Imports außer `ticketForm.js`).
|
||||
- [ ] **Step 4:** `npm test` → alle grün.
|
||||
- [ ] **Step 5:** Spec-Zeile anpassen; commit `git add src/lib/timeSteps.js src/lib/support.js tests/unit/support.test.mjs docs/superpowers/specs/2026-09-30-unterstuetzung-design.md && git commit -m "Unterstützung: Rechenregeln für 15-Minuten-Raster und Unterstützungszeit"`.
|
||||
|
||||
### Task 2: Appwrite-Attribut `supporters` (Skript für den Nutzer)
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/setup-supporters.mjs`
|
||||
- Test: `tests/setup-supporters.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `export const ATTRIBUTE = { key: 'supporters', type: 'string', size: 4000, required: false }`; `export function planAttribute(existing: {key,type,size}[]): 'create'|'exists'|'conflict'` (`conflict` = Key vorhanden mit anderem Typ oder Größe < 4000). CLI wie `scripts/setup-legal.mjs`: `--dry-run`, `APPWRITE_API_KEY`, Endpoint/Projekt/DB-Defaults identisch, Collection `worksheets`; bei `create` POST `/databases/woms-database/collections/worksheets/attributes/string`, danach Status pollen bis `available` (max. 30 s); bei `conflict` Exit 1 mit Meldung. Hauptteil nur, wenn als Skript gestartet (`import.meta.url === pathToFileURL(process.argv[1]).href`).
|
||||
|
||||
- [ ] **Step 1: Failing test:** `planAttribute([])` = 'create'; `[{key:'supporters',type:'string',size:4000}]` = 'exists'; `[{key:'supporters',type:'string',size:255}]` = 'conflict'; `[{key:'supporters',type:'integer'}]` = 'conflict'.
|
||||
- [ ] **Step 2:** `npm test` → FAIL.
|
||||
- [ ] **Step 3:** Skript implementieren.
|
||||
- [ ] **Step 4:** `npm test` grün; `node scripts/setup-supporters.mjs --dry-run` ohne Key → gibt geplanten Schritt aus, Exit 0, kein Netzwerkzugriff.
|
||||
- [ ] **Step 5:** Commit „Unterstützung: Setup-Skript für das Attribut supporters“.
|
||||
|
||||
### Task 3: Zeiten nur in 15-Minuten-Schritten
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/ui/DurationSelect.jsx` (+ Export in `src/components/ui/index.js`, falls dort gebündelt)
|
||||
- Modify: `src/components/CreateWorksheetModal.jsx` (Feld „Arbeitszeit (Minuten)“ Z. ~238–254, Auto-Berechnung Z. ~60–67, Submit Z. ~105), `src/components/WorksheetTimesModal.jsx` (Z. 21–31, 117–134), `src/hooks/useWorksheets.js` (`createWorksheet` Z. ~146–153, `updateWorksheetTimes` Z. ~222–231)
|
||||
- Test: `.superpowers/sdd/2026-09-30-unterstuetzung/ui.mjs` (Playwright, siehe Test-Rezept unten), `tests/unit/support.test.mjs` erweitern falls neue reine Hilfen
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `stepOptions`, `roundUpToStep`, `isValidStep`, `MAX_STEP_MINUTES` (Task 1), `minutesBetween` (ticketForm).
|
||||
- Produces: `<DurationSelect id value onChange disabled />` – `<select>` über `stepOptions(value)`, `onChange(minutes:number)`; Label „Arbeitszeit“.
|
||||
|
||||
Verhalten: Beide Modals rechnen automatisch `roundUpToStep(minutesBetween(start,end))`, solange nicht manuell gewählt; Hinweis darunter „✓ Aus Beginn und Ende berechnet, aufgerundet auf 15 Minuten“ bzw. „Von Hand gewählt“. `WorksheetTimesModal` startet mit `isValidStep(stored) ? stored : roundUpToStep(stored || berechnet)`. „Nur Kommentar“ deaktiviert die Auswahl und speichert 0. `createWorksheet` rundet eine fehlende/ungültige Zeit mit `roundUpToStep`; `createWorksheet` (nicht Kommentar) und `updateWorksheetTimes` lehnen `!isValidStep(totalTime)` ab mit „Die Arbeitszeit muss in 15-Minuten-Schritten angegeben werden.“
|
||||
|
||||
- [ ] **Step 1: Failing UI-Checks** in `ui.mjs`: (a) neues Arbeitsblatt 09:00–09:50 → Auswahl zeigt „1 h“, gespeichert `totalTime: 60`; (b) 23:30–00:15 → 45; (c) Auswahl hat 48 Optionen, keine freie Zahleneingabe (`input[type=number]` im Formular fehlt); (d) Zeiten bearbeiten bei `totalTime: 70` → „1 h 15 min“ vorausgewählt, Speichern schickt 75; (e) Git „Zeit nachtragen“ Beginn 14:00, Ende 14:37 → 45; (f) „Nur Kommentar“ → Auswahl disabled, gespeichert 0.
|
||||
- [ ] **Step 2:** Harness bauen und laufen lassen → Checks FAIL.
|
||||
- [ ] **Step 3:** `DurationSelect` bauen, in beiden Modals einsetzen, `useWorksheets` anpassen.
|
||||
- [ ] **Step 4:** `npm test` grün; `ui.mjs` (a)–(f) grün.
|
||||
- [ ] **Step 5:** Commit „Arbeitszeit nur in 15-Minuten-Schritten, berechnete Zeit wird aufgerundet“.
|
||||
|
||||
### Task 4: Unterstützung im Formular des Erstellers
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/SupportersEditor.jsx`
|
||||
- Modify: `src/components/WorksheetTimesModal.jsx`, `src/components/CreateWorksheetModal.jsx`, `src/hooks/useWorksheets.js`, `src/components/TicketCard.jsx` (Mitarbeiterliste an die Modals geben)
|
||||
- Test: `ui.mjs` erweitern
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `parseSupporters`, `serializeSupporters`, `validateSupporters`, `supportMinutes`, `stepLabel` (Task 1); `useEmployees()` → `employees` mit `userId`, `displayName`.
|
||||
- Produces: `<SupportersEditor value={Supporter[]} onChange={(list)=>void} employees={[]} excludeUserId={string} minutes={number} />` – Haken „Unterstützung“ (an, wenn `value.length`), Zeilen *Mitarbeiter* (`<select>`, ohne `excludeUserId`, ohne in anderen Zeilen gewählte, Mitarbeiter ohne `userId` weggelassen) + *Wobei?* (`maxLength 200`, required) + Entfernen; „+ weitere Person“; je Zeile Vorschau „= {stepLabel(supportMinutes(minutes))}“; Haken aus → `onChange([])`; keine Mitarbeiter → Hinweis „Mitarbeiterliste konnte nicht geladen werden.“
|
||||
- Produces (`useWorksheets`): `updateWorksheetTimes(worksheet, times, currentUser, employee)` akzeptiert zusätzlich `times.supporters: Supporter[]` und `times.supportersBefore: string` (Rohwert beim Öffnen). Vor dem Schreiben: `databases.getDocument` frisch; ist `fresh.supporters ?? ''` ≠ `supportersBefore ?? ''`, Abbruch mit `{ success:false, error:'Inzwischen hat jemand die Unterstützung geändert. Bitte das Formular schließen und neu öffnen.' }`. Sonst patcht es `supporters: serializeSupporters(list)` mit. `createWorksheet(data, currentUser)` übernimmt `data.supporters` (Array) serialisiert.
|
||||
|
||||
- [ ] **Step 1: Failing UI-Checks:** (g) Ersteller öffnet „Zeiten bearbeiten“ (60 min), setzt Haken, wählt Andrej „Farbkonzept“ und Nico „Texte“ → PATCH enthält `supporters` mit beiden, Vorschau je „= 30 min“; (h) eigene Person fehlt in der Auswahl, schon gewählte Person fehlt in der zweiten Zeile; (i) leeres „Wobei?“ → Speichern blockiert mit Fehlertext aus Task 1; (j) Mock ändert `supporters` zwischen Öffnen und Speichern → Konfliktmeldung, kein PATCH; (k) neues Arbeitsblatt mit Unterstützung → `createDocument` enthält `supporters`; (l) Employees-Mock liefert 500 → Hinweistext sichtbar.
|
||||
- [ ] **Step 2:** → FAIL.
|
||||
- [ ] **Step 3:** Implementieren.
|
||||
- [ ] **Step 4:** `npm test` + `ui.mjs` grün (a)–(l).
|
||||
- [ ] **Step 5:** Commit „Unterstützung: Ersteller trägt helfende Personen im Arbeitsblatt ein“.
|
||||
|
||||
### Task 5: „Ich habe unterstützt“
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/OwnSupportModal.jsx`
|
||||
- Modify: `src/components/WorksheetList.jsx` (Kopf-Knöpfe Z. ~164–200), `src/components/TicketCard.jsx`, `src/hooks/useWorksheets.js`
|
||||
- Test: `ui.mjs` erweitern
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `canAddOwnSupport`, `parseSupporters`, `upsertSupporter`, `removeSupporter`, `serializeSupporters` (Task 1).
|
||||
- Produces (`useWorksheets`): `saveOwnSupport(worksheet, task: string|null, currentUser, employee): Promise<{success, error?}>` – prüft `canAddOwnSupport`, liest das Dokument frisch (`getDocument`), setzt/ersetzt den Eintrag `{ employeeId: user.$id, name: employee?.displayName || user.name, task }` bzw. entfernt ihn bei `task === null`, patcht nur `{ supporters }`, aktualisiert den State.
|
||||
- Produces (`WorksheetList` Props): `canAddSupport(ws): boolean`, `onOwnSupport(ws): void`; Knopf-Text „Ich habe unterstützt“ bzw. „Meine Unterstützung bearbeiten“, wenn `parseSupporters(ws.supporters)` die eigene `user.$id` enthält (dafür neue Prop `currentUserId`).
|
||||
- `OwnSupportModal`: Titel „Unterstützung eintragen“, Feld „Wobei?“ (required, 200), Anzeige „Dir werden {stepLabel(supportMinutes(ws.totalTime))} angerechnet.“, Knöpfe „Speichern“ und – wenn schon eingetragen – „Eintrag entfernen“.
|
||||
|
||||
- [ ] **Step 1: Failing UI-Checks:** (m) Nicht-Ersteller sieht bei fremdem 60-min-Blatt „Ich habe unterstützt“, Ersteller nicht; (n) Speichern mit „Design geprüft“ → vor dem PATCH ein GET des Dokuments; PATCH-Body enthält nur `supporters` mit dem bestehenden Eintrag (vom Mock) **und** dem neuen; (o) danach heißt der Knopf „Meine Unterstützung bearbeiten“, „Eintrag entfernen“ entfernt nur den eigenen; (p) Git-Push ohne Zeit und Kommentar: kein Knopf; Git-Push mit Zeit von `knso`: Kenso sieht „Zeiten bearbeiten“, Andrej sieht „Ich habe unterstützt“.
|
||||
- [ ] **Step 2:** → FAIL.
|
||||
- [ ] **Step 3:** Implementieren.
|
||||
- [ ] **Step 4:** `npm test` + `ui.mjs` grün.
|
||||
- [ ] **Step 5:** Commit „Unterstützung: Mitarbeiter tragen sich selbst als Unterstützung ein“.
|
||||
|
||||
### Task 6: Anzeige in WOMS
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/WorksheetList.jsx` (eingeklappter Kopf Z. ~146–155 und aufgeklappter Körper Z. ~214–240), `src/components/WorksheetSummary.jsx`, `src/hooks/useWorksheets.js` (`getTotalTime` Z. ~255–259)
|
||||
- Test: `ui.mjs` erweitern
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `parseSupporters`, `supportMinutes`, `worksheetMinutes`, `firstName` (Task 1).
|
||||
|
||||
Verhalten: Aufgeklappt unter Mitarbeiter/Zeit je Eintrag eine Zeile „↳ Unterstützung Andrej · Farbkonzept · 30min“ (Zeit mit dem vorhandenen `formatTime`). Eingeklappt bei vorhandener Unterstützung zusätzlich kleines Badge „+{n} Unterstützung“. `WorksheetSummary` und `getTotalTime` summieren `worksheetMinutes(ws)` über `!ws.isComment`.
|
||||
|
||||
- [ ] **Step 1: Failing UI-Checks:** (q) Blatt 60 min mit 2 Unterstützern → zwei Zeilen „↳ Unterstützung Andrej · Farbkonzept · 30min“ / „↳ Unterstützung Nico · Texte · 30min“, Kopf-Badge „+2 Unterstützung“; (r) Summe „Arbeitszeit gesamt 2h 0min“ bei diesem einen Blatt; (s) `supporters: 'kaputt'` → Blatt rendert normal ohne Zeilen, keine Konsolen-Exception.
|
||||
- [ ] **Step 2:** → FAIL.
|
||||
- [ ] **Step 3:** Implementieren.
|
||||
- [ ] **Step 4:** `npm test` + `ui.mjs` grün; Deploy-naher Build: `git archive HEAD | …` in `docker run --rm node:20-alpine` (`npm install && npm run build`) Exit 0.
|
||||
- [ ] **Step 5:** Commit „Unterstützung: Anzeige in Arbeitsblatt-Liste und Gesamtzeit“.
|
||||
|
||||
**WOMS-Test-Rezept** (aus Memory, `ui.mjs`): esbuild-Harness mit JS-API (`require('<worktree>/node_modules/esbuild').build`), `define` für `import.meta.env.*`, Loader `.js=jsx`, `.ttf`/`.woff2`, Stub-Plugin für ProjectContextPanel/TicketProjects/InvoicePanel; Bau unter `systemd-run --scope -p MemoryMax=450M -p MemorySwapMax=0`, Playwright headless unter MemoryMax 700M, Bundle über `page.route` von `https://ticket.test`, Appwrite-REST (`/v1/account`, `/v1/databases/woms-database/collections/{worksheets,employees}/documents[/:id]`) gemockt, PATCH/POST-Bodies in `window.__calls` protokolliert. `set -o pipefail`, Exit-Codes prüfen.
|
||||
|
||||
---
|
||||
|
||||
## Kundenportal
|
||||
|
||||
### Task 7: Rechenregel + Stundenkonto
|
||||
|
||||
**Files:**
|
||||
- Setup: `git -C /home/webklar/apps/kundenbereich.webklar.com/build fetch origin && git -C … worktree add -b feature/unterstuetzung ../dev-unterstuetzung origin/main`, dann node_modules-Symlink.
|
||||
- Create: `server/services/worksheetMinutes.js`
|
||||
- Modify: `server/services/hourLedger.js` (`listPlanUsage` Z. 54–70), `public/app.js` (`renderHoursView` Z. ~1218–1221)
|
||||
- Test: `test/worksheetMinutes.test.js`, `test/hourLedger.test.js` (neu, falls nicht vorhanden; sonst erweitern)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `supportMinutes(total)`, `parseSupporters(raw)`, `worksheetMinutes(ws)`, `firstName(name)` – gleiche Semantik und Testvektoren wie WOMS Task 1; `parseSupporters` loggt bei kaputtem Wert einmal `console.warn('[unterstuetzung] supporters unlesbar', ws?.$id)` über optionalen zweiten Parameter `context`.
|
||||
- Produces (`listPlanUsage` Eintrag): zusätzlich `supportMinutes: number` (Summe der Unterstützung); `rawMinutes = worksheetMinutes(ws)`, `minutes = roundUsage(rawMinutes)`; Filter `worksheetMinutes(ws) > 0`.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** Vektoren; `parseSupporters` wie WOMS; `listPlanUsage` mit gemocktem `listDocuments` (Muster aus `test/support/appwriteMock`) – Blatt 60 min + 1 Unterstützer → `minutes 90, supportMinutes 30`; Blatt mit `supporters:'kaputt'` → `minutes 60`, kein Throw; `computeLedger` mit 60 min Guthaben und diesem 90-min-Eintrag → `overageMinutes 30`.
|
||||
- [ ] **Step 2:** `npm test` → FAIL.
|
||||
- [ ] **Step 3:** Implementieren; in `renderHoursView` hinter `−{fmtHours(u.minutes)}` bei `u.supportMinutes` ein `<span class="muted">inkl. {fmtHours(u.supportMinutes)} Unterstützung</span>`.
|
||||
- [ ] **Step 4:** `npm test` grün (bekannt flaky/unabhängig: `repoMove.test.js`).
|
||||
- [ ] **Step 5:** Commit „Unterstützung: Rechenregel und Abzug im Abo-Stundenkonto“.
|
||||
|
||||
### Task 8: Projektansicht – Unterstützungs-Zeilen und Gesamtzeit
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/services/customerProjectView.js` (`customerWorkEntry` Z. ~197–216, `customerWorkEntries` Z. ~219–233), `server/routes/projects.js` (`GET /:id/work` Z. ~78–95), `public/app.js` (`renderWorkEntry` Z. ~319–336, `renderOrder` Z. ~252–273, Projektdetails Z. ~386–396), `public/kundenbereich.css`, `server/services/customerActivity.js` (Z. ~36–47), `public/admin.js` (Z. ~167)
|
||||
- Test: `test/customerProjectView.test.js` erweitern
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `parseSupporters`, `supportMinutes`, `worksheetMinutes`, `firstName` (Task 7).
|
||||
- Produces: Work-Eintrag bekommt `supports: { name: string /*Vorname*/, task: string, minutes: number }[]` (leer = `[]`); zusammengefasste Git-Tage hängen `supports` aneinander; `minutes` des Eintrags bleibt die eigene Arbeitszeit (ohne Unterstützung). Neue Export-Funktion `workTotalMinutes(entries): number` = Σ (`minutes` + Σ `supports.minutes`). `GET /:id/work` antwortet zusätzlich `totalMinutes`. `customerActivity` liefert je Blatt zusätzlich `supportMinutes` (Summe); `admin.js` zeigt dann „, +{n} min Unterstützung“.
|
||||
- Anzeige: `renderWorkEntry` hängt je Unterstützung `<li class="pd-support">↳ Unterstützung {name}: {task} <span class="pd-time">{fmtMinutes}</span></li>` in einer `<ul class="pd-supports">` unter den Eintrag. Kopf: bei `order` in `pd-order-head` ein `<span class="pd-total">Arbeitszeit gesamt: {fmtMinutes(totalMinutes)}</span>`; ohne `order` als erste Zeile des Abschnitts „Arbeiten am Projekt“; bei 0 nichts.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** normales Blatt 60 min + Andrej „Farbkonzept“ → `supports: [{name:'Andrej', task:'Farbkonzept', minutes:30}]`, `minutes 60`; zwei Git-Pushs am selben Tag je mit einem Unterstützer → ein Eintrag mit zwei `supports`; `workTotalMinutes` über diese Einträge; Kommentar mit `supporters` → kein Eintrag; `supporters:'kaputt'` → `supports: []`.
|
||||
- [ ] **Step 2:** → FAIL.
|
||||
- [ ] **Step 3:** Implementieren (Server + `app.js` + CSS: `.pd-supports` eingerückt, gleiche Schriftgröße wie `.pd-entry-meta`).
|
||||
- [ ] **Step 4:** `npm test` grün; UI-Blick mit dem statischen Fixture-Server aus [[kundenportal-testing]] (Projektdetails mit `work`-Fixture inkl. `totalMinutes` und `supports`, Desktop + 390 px Breite, Screenshot in den SDD-Ordner).
|
||||
- [ ] **Step 5:** Commit „Unterstützung: eigene Zeilen und Gesamtzeit in den Projektdetails“.
|
||||
|
||||
### Task 9: Push-Zeitmail nur in 15-Minuten-Schritten
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/services/pushTimeEntry.js` (`parseMinutes` Z. ~40–47), `server/routes/pushTime.js` (Formular Z. ~153–156)
|
||||
- Test: `test/pushTimeEntry.test.js` bzw. `test/pushTimeRoute.test.js` erweitern
|
||||
|
||||
**Interfaces:** `parseMinutes(raw)` akzeptiert nur ganze Vielfache von 15 in 15–720, sonst `null`. Das Formular wird ein `<select name="minutes">` mit 15 … 720 in 15er-Schritten (Label wie `formatMinutes`), vorausgewählt `minutes` aus der Query bzw. 60. `QUICK_MINUTES` bleibt.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** `parseMinutes('45')`=45, `'720'`=720, `'70'`=null, `'0'`=null, `'735'`=null; Route-Test: GET `/zeit/<token>` enthält `<select name="minutes"` mit 48 `<option`; POST `minutes=70` → Fehlerseite, kein Schreiben.
|
||||
- [ ] **Step 2:** → FAIL.
|
||||
- [ ] **Step 3:** Implementieren; Fehlertext „Bitte eine Zeit in 15-Minuten-Schritten wählen.“
|
||||
- [ ] **Step 4:** `npm test` grün.
|
||||
- [ ] **Step 5:** Commit „Push-Zeitmail: Zeit nur in 15-Minuten-Schritten“.
|
||||
|
||||
### Task 10: Abo-Bedingungen Fassung 5
|
||||
|
||||
**Gate:** Vor Step 3 den endgültigen § 4-Text (Spec, Abschnitt Verträge) dem Nutzer im Chat vorlegen und auf sein OK warten.
|
||||
|
||||
**Files:**
|
||||
- Create: `public/vertragsunterlagen/v4/abo-bedingungen.html` + `public/vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf` (bytegleiche Kopien der heutigen Fassung 4)
|
||||
- Modify: `public/abo-bedingungen.html` (§ 4 neuer Satz nach der Rundungsregel; Kopf „Version 5 · Stand {Datum}“ an beiden Stellen), `server/config.js` (`termsVersion: '{yyyy-mm-dd}-v5'` Z. ~158), `server/services/products.js` (`isTermsV4`/`termsDocFor` Z. ~28–55), `scripts/build-legal-pdfs.sh`, `public/WEBklar-Abo-Vertragsunterlagen.pdf` (neu erzeugt), `test/legalTexts.test.js`, `test/products.test.js`, alle Fundstellen von `isTermsV4`/`-v4` (`server/routes/abo.js`, `test/mailTemplates.test.js`, `server/mail-templates/K01 Vertragsbestaetigung.html`)
|
||||
- `{Datum}` = Tag der Umsetzung; verschiebt sich der Livegang, im Livegang-Schritt anpassen.
|
||||
|
||||
**Interfaces:**
|
||||
- Produces (`products.js`): `isCurrentTerms(v): boolean` (endet auf `-v5`), `isTermsV4OrNewer(v): boolean` (endet auf `-v4` oder `-v5`), `termsDocFor(v)`: `-v5` → `{ url:'/abo-bedingungen.html', pdfFile:'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph:'§ 11' }`; `-v4` → `{ url:'/vertragsunterlagen/v4/abo-bedingungen.html', pdfFile:'vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph:'§ 11' }`; sonst v3 wie bisher. `contractAttachmentFor` liefert als `filename` den Basisnamen. Alle bisherigen Aufrufer von `isTermsV4`, die „aktuelle Fassung“ meinen, nutzen `isCurrentTerms`; wo „Fassung 4 oder neuer“ gemeint ist (Hosting/Zusatzspeicher/Wechsel), `isTermsV4OrNewer`. `isTermsV4` entfällt.
|
||||
|
||||
- [ ] **Step 1: Failing tests** (`legalTexts.test.js`): Fassung 4 bytegleich archiviert (sha256 HTML = `git show origin/main:public/abo-bedingungen.html`, fest eingetragen wie V3_SHA256); Fassung 5 enthält „Version 5“, nicht „Version 4“, § 4 enthält „die Hälfte der Arbeitszeit dieses Arbeitsschritts“, „aufgerundet auf volle 15 Minuten“, „jede Unterstützung mit Person, Tätigkeit und angerechneter Zeit“; Widerrufsbelehrung + Formular wortgleich mit Fassung 4. `products.test.js`: `termsDocFor` für `-v5`, `-v4`, `-v3`, `undefined`.
|
||||
- [ ] **Step 2:** `npm test` → FAIL.
|
||||
- [ ] **Step 3:** Nach OK des Nutzers: Archiv anlegen, Text einfügen, Version hochziehen, Code anpassen, PDFs mit `scripts/build-legal-pdfs.sh` neu erzeugen. Weitere Fundstellen der Rundungs-/Stundenregel suchen (`grep -rn "15 Minuten" public server/mail-templates`), bei Abweichung angleichen.
|
||||
- [ ] **Step 4:** `npm test` grün; PDF öffnen und § 4 prüfen (Seitenumbruch, Kopfzeile „Version 5“).
|
||||
- [ ] **Step 5:** Commit „Abo-Bedingungen Fassung 5: Unterstützung in § 4, Fassung 4 archiviert“.
|
||||
|
||||
## webklar.com
|
||||
|
||||
### Task 11: AGB § 9 Absatz 2
|
||||
|
||||
**Gate:** Endgültigen Text dem Nutzer vorlegen (zusammen mit Task 10 möglich), OK abwarten.
|
||||
|
||||
**Files:**
|
||||
- Vorher: `git -C /home/webklar/apps/webklar.com/dev-agb-haftung fetch origin && git … rebase origin/main` (f40e889 liegt noch auf 1637a4a; main ist 3c9a879), node_modules-Symlink prüfen.
|
||||
- Modify: `src/pages/AGB.tsx` (§ 9, Z. ~188–198: neuer zweiter Listenpunkt, bisherige 2 und 3 werden 3 und 4)
|
||||
- Test: `src/test/agb-verguetung.test.tsx`
|
||||
|
||||
Text (vollständig ausgeschrieben, kein Verweis): „Wird nach Aufwand abgerechnet, erfasst WEBklar die Arbeitszeit je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. Wirkt eine weitere Person unterstützend mit, rechnet WEBklar für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede Unterstützung ist im Kundenportal einsehbar.“ (AGB sprechen von „WEBklar“ in der dritten Person, daher angepasst.)
|
||||
|
||||
- [ ] **Step 1: Failing test:** § 9 hat 4 Absätze; Absatz 1 unverändert „Die Vergütung ergibt sich aus dem Angebot.“; Absatz 2 = Text oben; Absatz 4 „Für die Abos gelten die Vertragsbedingungen für die WEBklar-Abos.“
|
||||
- [ ] **Step 2:** `npx vitest run src/test/agb-verguetung.test.tsx` → FAIL.
|
||||
- [ ] **Step 3:** Absatz einfügen; prüfen, ob irgendwo auf „§ 9 Abs. 2/3“ der AGB verwiesen wird (`grep -rn "9 Abs" src` in webklar.com und `public/` des Portals) und anpassen.
|
||||
- [ ] **Step 4:** `npx vitest run` grün (inkl. `agb-haftung.test.tsx`); `npm run build` Exit 0.
|
||||
- [ ] **Step 5:** Commit „AGB § 9: Zeiterfassung in 15-Minuten-Schritten und Unterstützung“.
|
||||
|
||||
## Livegang (nur mit OK des Nutzers, in dieser Reihenfolge)
|
||||
|
||||
- [ ] 1. Nutzer: `cd /home/webklar/apps/ticket.webklar.com/dev-unterstuetzung && APPWRITE_API_KEY=… node scripts/setup-supporters.mjs` → „available“.
|
||||
- [ ] 2. Portal: `git -C …/kundenbereich.webklar.com/dev-unterstuetzung push origin feature/unterstuetzung:main`; prüfen: Deploy-Marker/Commit im Container, `/api/projects/:id/work` liefert `totalMinutes`, Stundenkonto lädt, `/abo-bedingungen.html` zeigt „Version 5“, `/vertragsunterlagen/v4/abo-bedingungen.html` erreichbar.
|
||||
- [ ] 3. WOMS: `git -C …/ticket.webklar.com/dev-unterstuetzung push origin feature/unterstuetzung:test`; prüfen: Live-Bundle enthält „Ich habe unterstützt“, Arbeitszeit-Auswahl vorhanden.
|
||||
- [ ] 4. webklar.com: `git -C …/webklar.com/dev-agb-haftung push origin fix/agb-haftung:main`; `/agb` zeigt § 9 Abs. 2 und § 11 neu.
|
||||
- [ ] 5. Nutzer lädt in WOMS „Rechtliches“ die neuen Versionen hoch (Abo-Bedingungen v5 PDF, AGB neue Version) mit Änderungsnotiz.
|
||||
- [ ] 6. Lesender Check mit echten Daten im Chrome des Nutzers: ein Arbeitsblatt mit Unterstützung anlegen (Test-Ticket), Portal-Projektansicht zeigt die Zeile und Gesamtzeit.
|
||||
- [ ] 7. Memory aktualisieren (woms-ticketsystem, webklar-kundenportal, webklar-website).
|
||||
@@ -0,0 +1,274 @@
|
||||
# WOMS „Weißes Design“ – Übersicht der Umsetzungspläne
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Das ganze Ticketsystem WOMS bekommt das freigegebene weiße Design (Akzent Schwarz, kaum Rahmen, kaum Icons). Ein Ticket öffnet künftig als eigene Seite. Die Umsetzung kommt in elf einzeln auslieferbaren Paketen.
|
||||
|
||||
**Architecture:**
|
||||
- P1 legt Tokens, Gerüst, Bausteine und eine Übergangsschicht (KOMPAT) in `global.css` an. Danach ist die App sofort weiß.
|
||||
- P2 bis P9 bauen je einen Bereich auf die neuen Bausteine um.
|
||||
- P10 räumt Altlasten und Icon-Bibliotheken ab.
|
||||
- Jedes Paket geht nur über ein Git-Tag live.
|
||||
|
||||
**Tech Stack:**
|
||||
- React 18
|
||||
- react-router-dom 6
|
||||
- Vite 5
|
||||
- Appwrite Web SDK 13 (Legacy-Query-Syntax)
|
||||
- reines CSS ohne Framework
|
||||
- Tests: `node --test` mit esbuild-Lader (`tests/unit/load.mjs`)
|
||||
- Screenshots: playwright-core 1.62.1 und chromium-headless-shell 1234
|
||||
|
||||
**Spec:**
|
||||
- `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, verbindlich
|
||||
- Anhang `docs/superpowers/specs/2026-10-04-weisses-design-umbaukarte.md`
|
||||
- Prototyp: `/root/reports/ticketsystem-design-2026-10-04/quelle/` (`styles.css`, `app.js`)
|
||||
|
||||
## Pläne
|
||||
|
||||
| Paket | Datei | Kurz |
|
||||
|---|---|---|
|
||||
| P0 | `2026-10-05-weisses-design-p0-vorbereitung.md` | Worktree lauffähig, Totcode, Screenshot-Werkzeug |
|
||||
| P1 | `2026-10-05-weisses-design-p1-fundament.md` | Tokens, KOMPAT, Gerüst, Menü, Bausteine, Dialog, Login, Pflicht-Fixes |
|
||||
| P2 | `2026-10-05-weisses-design-p2-tickets.md` | Ticketliste, Ticket-Seite, Arbeitsblätter, Ticket-Dialoge |
|
||||
| P3 | `2026-10-05-weisses-design-p3-planboard.md` | Planboard |
|
||||
| P4 | `2026-10-05-weisses-design-p4-kunden.md` | Kundenliste, Kundenseite |
|
||||
| P5 | `2026-10-05-weisses-design-p5-projekte.md` | Projekte, Zuordnen, Services, Projekt-Bausteine |
|
||||
| P6 | `2026-10-05-weisses-design-p6-kunden-unterseiten.md` | Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher |
|
||||
| P7 | `2026-10-05-weisses-design-p7-finanzen.md` | Finanzen |
|
||||
| P8 | `2026-10-05-weisses-design-p8-rechtliches-admin.md` | Rechtliches, Admin |
|
||||
| P9 | `2026-10-05-weisses-design-p9-serverpuls.md` | Serverpuls |
|
||||
| P10 | `2026-10-05-weisses-design-p10-aufraeumen.md` | KOMPAT, Aliase, Icon-Pakete entfernen |
|
||||
|
||||
## Reihenfolge
|
||||
|
||||
- Die Pakete werden strikt in der Reihenfolge P0 → P10 umgesetzt.
|
||||
- P0 geht nicht allein live, sondern zusammen mit P1. Das Tag `design-p1` enthält die P0-Commits.
|
||||
- An Paket N+1 wird erst gearbeitet, wenn `design-p<N>` gesetzt ist.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Diese Regeln gelten für jede Aufgabe aller Pakete.
|
||||
|
||||
**Befehle und Prüfungen**
|
||||
- Jede Bash-Ausführung beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Das Skript (aus P0) setzt:
|
||||
- `WT=/home/webklar/apps/ticket.webklar.com/dev-design`
|
||||
- `W=/root/reports/ticketsystem-design-2026-10-04/werkzeug`
|
||||
- `TMPDIR=$W/tmp`
|
||||
- `set -o pipefail`
|
||||
- `cd "$WT"`
|
||||
|
||||
Temporäre Dateien liegen nie direkt in `/tmp`. Auch die ```bash-Commit-Blöcke der Pläne laufen in einem Shell-Aufruf, der zuerst `env.sh` lädt.
|
||||
- Tests und Build werden nur über `"$W/check.sh"` als Bedingung genutzt. Varianten:
|
||||
- `"$W/check.sh"` für Tests und Build
|
||||
- `"$W/check.sh" --nur-tests`
|
||||
- `"$W/check.sh" --nur-build`
|
||||
|
||||
Das Skript gibt die Zeilen `# pass`/`# fail` und die Bundle-Größen aus und endet mit dem Exit-Code von `npm test` bzw. `npm run build`. Ein Tag wird nur mit `"$W/check.sh" && git tag design-p<N>` gesetzt. Ketten wie `npm test | grep … && …` sind verboten, weil sie den Exit-Code von `grep` werten.
|
||||
- Browser-Skripte (Playwright, Pixelvergleich) laufen nie direkt mit `node`, sondern über die Hüllen `$W/shots.sh` oder `$W/run-browser.sh <skript> [args]`. Beide starten unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0`.
|
||||
- `rg` auf diesem Host ist Version 13.0.0 ohne PCRE2. Deshalb kein `rg -P`, keine Lookarounds.
|
||||
|
||||
**CSS-Reihenfolge**
|
||||
- `src/main.jsx` importiert `App` vor `./styles/global.css`. Deshalb stehen alle von Komponenten importierten Stylesheets im Bundle **vor** `global.css`: `chat.css`, `serverpuls.css`, `admin-areas.css`, `pages/AdminPage.css` und `uplot.min.css`.
|
||||
- Bei gleicher Spezifität gewinnt `global.css`. Bereichs-CSS, das eine globale Regel überstimmen muss, braucht eine höhere Spezifität (z. B. `.page.admin-page`) und nie `!important`.
|
||||
|
||||
**Parallele Arbeit an WOMS**
|
||||
- Auf `feature/terminplaner-tp2` arbeitet eine andere Sitzung weiter. Stand 2026-10-05: 5 Commits vor `test`, sie berühren u. a. `CustomerFunktionen.jsx`, `lib/funktionenView.js` und Rechtliches.
|
||||
- Vor dem Start jedes Pakets wird `git log --oneline origin/test..feature/terminplaner-tp2 -- <Dateien des Pakets>` geprüft. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
|
||||
|
||||
**Paketstart und Abnahme-Reihenfolge**
|
||||
- Paketstart:
|
||||
- `git fetch origin`
|
||||
- `git merge-base --is-ancestor design-p<N-1> HEAD`, sonst stoppen und melden
|
||||
- `git rebase origin/test`
|
||||
- `"$W/check.sh"`
|
||||
- Abnahme: erst `git fetch origin && git rebase origin/test`, dann Gate-Lauf, Screenshots und Review. So zeigen die Bilder den Stand, der getaggt wird.
|
||||
- Jede Übergabe nennt neben der Push-Zeile auch die Rücknahme: `git revert --no-commit design-p<N-1>..design-p<N>` als Commit auf `test`. Das gilt nur für das jüngste Paket.
|
||||
|
||||
**Bausteine benutzen**
|
||||
- Jeder Knopf in einem `<Dialog as="form">` außer dem Absenden-Knopf trägt `type="button"`. `Button` setzt keinen eigenen Standard-Typ, weil Bestandsformulare darauf bauen.
|
||||
- Abstände zwischen aufeinanderfolgenden Abschnitten regelt P1 global mit `.page section + section { margin-top: 36px }`. Pakete bauen dafür keine eigenen Regeln. Eigene Abstände sind nur innerhalb von Bereichs-Containern erlaubt, die keine `section` sind.
|
||||
- Klickbare Tabellenzeilen prüfen Klicks immer mit `isRowActivation(event)` aus `src/lib/rowClick.js` (entsteht in P2, Task 2). Kein Paket baut diese Regel neu.
|
||||
|
||||
**Commit-Trailer**
|
||||
- In den Commit-Beispielen steht `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. Maßgeblich ist die Attribution, die die ausführende Sitzung vorgibt.
|
||||
|
||||
**Arbeitsort und Git**
|
||||
- Gearbeitet wird nur in `/home/webklar/apps/ticket.webklar.com/dev-design` auf dem Branch `feature/design-weiss`. In `/home/webklar/apps/ticket.webklar.com/build/` (Live-Checkout) wird nie etwas geändert.
|
||||
- `git add` nur mit ausdrücklichen Pfaden, nie `-A` oder `.`.
|
||||
- Commit-Nachrichten auf Deutsch, letzte Zeile `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
|
||||
- Niemals `git push`. Live bringt nur der Eigentümer mit `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p<N>^{commit}:refs/heads/test'`.
|
||||
- Vor dem Paketstart und vor dem Setzen des Tags: `git fetch origin && git rebase origin/test`. Das Tag `design-p<N>` wird danach gesetzt und nie bewegt. Korrekturen nach dem Push: `design-p<N>-fix<k>`.
|
||||
- Nicht anfassen:
|
||||
- `.env*` (Latin-1)
|
||||
- das Portal-Repo
|
||||
- `src/lib/loadChatComposer.js` samt Test
|
||||
- die Container-Klassen in `src/components/chat/ChatThread.jsx` (`woms-chat`, `chat-messages`)
|
||||
- Appwrite-Schema und gespeicherte Werte
|
||||
|
||||
**Speicher und Prozesse**
|
||||
- Speicherintensives (`npm ci`, `npm run build`, Playwright) läuft unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0`, ein Build zusätzlich mit `NODE_OPTIONS=--max-old-space-size=1200`.
|
||||
- Prozesse nur per PID beenden, nie mit `pkill -f` (beendet sonst die eigene Shell).
|
||||
|
||||
**Tests und Screenshots**
|
||||
- Tests laufen ohne Produktionszugriff. Das Screenshot-Werkzeug blockiert jede Anfrage an einen echten Host.
|
||||
- Lauf: `npm test`, alle Tests grün.
|
||||
- Neue Lib-Funktionen bekommen Tests unter `tests/unit/<name>.test.mjs` im Stil der bestehenden Tests (`node:test`, `assert/strict`, `loadModule`).
|
||||
|
||||
**Gestaltung**
|
||||
- Akzent `#111111`. Pro Seitenkopf höchstens ein `.btn-primary`.
|
||||
- Breakpoints 1100 px (`.hide-md`) und 860 px (`.hide-sm`, Schublade, Bottom-Sheet).
|
||||
- Die Token-Werte stehen exakt in der Spec, Abschnitt „Design-Tokens“, und werden nicht neu erfunden.
|
||||
- Keine Icon-Komponenten in umgebauten Dateien. Handelnde Zeichen werden Text: „ד, „←“, „→“, „↑“, „↓“.
|
||||
- Keine neuen `style={{…}}` außer für Werte, die erst zur Laufzeit feststehen.
|
||||
- Kein Farbliteral in JSX. Farben nur über Tokens. Einzige Ausnahme: Canvas-Farben in `src/components/serverpuls/chartColors.js`.
|
||||
|
||||
**Texte und Verhalten**
|
||||
- Texte bleiben wörtlich. Geändert wird nur, was die Spec unter „Texte“ und in den Bausteinen nennt.
|
||||
- Rechtstexte, Mail- und Chat-Vorlagen, Fehlermeldungen und der Wortlaut in `lib/analyseView.js` bleiben unverändert.
|
||||
- Verhalten bleibt, außer „Bewusste Änderungen am Verhalten“ der Spec. Alle `window.confirm`-Rückfragen, `data-testid`, Doppelklick-Sperren (`busyRef`, `lockRef`) und URL-Parameter bleiben.
|
||||
|
||||
**Abschluss jedes Pakets**
|
||||
- Abnahme nach Spec „Abnahme“, Punkte 1 bis 7.
|
||||
- Code-Review mit dem Modell `sonnet`. Wichtige Befunde werden behoben, Kleinigkeiten notiert.
|
||||
|
||||
## Gemeinsame Schnittstellen aus P1
|
||||
|
||||
Diese Bausteine und Klassen sind ab P1 vorhanden. Spätere Pakete benutzen sie und erfinden keine eigenen.
|
||||
|
||||
**Komponenten**
|
||||
|
||||
- **`src/components/ui/Button.jsx`**
|
||||
- Signatur: `Button({ variant = 'default', size, as = 'button', className = '', ...props })`.
|
||||
- Varianten: `'default' | 'ghost'` → `btn`, `'primary'` → `btn btn-primary`, `'quiet'` → `btn btn-quiet`, `'danger'` → `link-danger`.
|
||||
- `size` wird ignoriert.
|
||||
- **`src/components/ui/Badge.jsx`**
|
||||
- Signatur: `Badge({ tone = 'muted', className = '', style, children })`.
|
||||
- Tonzuordnung: `muted|grey` → `''`, `info|blue` → `blue`, `ok|green` → `green`, `warn|orange` → `orange`, `danger|red` → `red`, dazu `violet` und `plain`.
|
||||
- Die Prop `dot` wird ignoriert.
|
||||
- **`src/components/ui/Card.jsx`**
|
||||
- Signatur: `Card({ title, actions, className, children })`.
|
||||
- Rendert `<section className={className}>`, mit Titel `<h2 className="section"><span>{title}</span>{actions && <span className="actions">{actions}</span>}</h2>`.
|
||||
- Ohne `title` und ohne `actions` entfällt das `h2`, die Kinder stehen direkt in `<section>`.
|
||||
- `pad`, `bodyStyle` und `style` werden ignoriert.
|
||||
- **`src/components/ui/PageHeader.jsx`**
|
||||
- Signatur: `PageHeader({ title, subtitle, actions, eyebrow, crumb, children })`.
|
||||
- `crumb` ist ein Array `[{ label: string, to?: string }]`, gerendert als `.crumb` mit „/“ in `--text-3` vor `.page-head`.
|
||||
- **`src/components/ui/Tabs.jsx`**
|
||||
- Signatur: `Tabs({ tabs: [{ id, label, count? }], active, onChange })`.
|
||||
- Rendert `.tabs`, `role="tablist"`, Buttons `.tab` mit `role="tab"`, `aria-selected`, Zähler `<span class="count">` bei `count != null`.
|
||||
- **`src/components/ui/EmptyState.jsx`**: `EmptyState({ title, hint, action })` → `.empty`.
|
||||
- **`src/components/ui/Field.jsx`**: `Field({ label, htmlFor, hint, children })` → `.field`, `.hint`.
|
||||
- **`src/components/ui/Stat.jsx`**
|
||||
- Signatur: `Stat({ label, value, sub, tone = '' })`, wobei `tone` `'' | 'warn' | 'neg'` ist.
|
||||
- Rendert `.stat` mit `.k` (Label), `.v {tone}` (Wert) und `.sub`.
|
||||
- Der Container `.stats` kommt vom Aufrufer.
|
||||
- **`src/components/ui/Kpi.jsx`**
|
||||
- Signatur: `Kpi({ label, value, sub, icon, accent })`.
|
||||
- Gibt `<Stat … tone={accent === 'var(--danger)' ? 'neg' : ''}>` zurück und ignoriert `icon`.
|
||||
- Wird in P7 gelöscht.
|
||||
- **`src/components/ui/StatusPill.jsx`**
|
||||
- `StatusPill({ status })` → `<span className={'badge ' + statusTone(status)}>{statusLabel(status)}</span>`.
|
||||
- `UrgentPill({ priority })` → `badge red` „Dringend“, nur wenn `isUrgent(priority)`.
|
||||
- **`src/components/ui/Dialog.jsx`**
|
||||
- Signatur: `Dialog({ title, onClose, busy = false, wide = false, as = 'div', onSubmit, footer, children })`.
|
||||
- Aufbau: Overlay `.overlay`, Box `.dialog` (`.dialog-wide`), Kopf `.dialog-head` mit `<h3 id>` und `<button type="button" className="dialog-close" aria-label="Schließen" disabled={busy}>×</button>`, darunter `.dialog-body` und `.dialog-foot`.
|
||||
- Mit `as="form"` ist die Box ein `<form onSubmit>`, die native Pflichtfeld-Prüfung bleibt aktiv.
|
||||
- Schließen: Esc nur bei `!e.defaultPrevented && !busy`. Ein Klick auf den Hintergrund schließt nie.
|
||||
- Fokus über `useDialogFocus`.
|
||||
- **`src/components/Logo.jsx`**: `Logo({ size = 24 })` mit dem SVG aus dem Prototyp (`app.js`, Konstante `LOGO`).
|
||||
- **`src/lib/ticketStatus.js`**: `statusTone(status) → '' | 'blue' | 'orange' | 'violet' | 'green' | 'plain'`.
|
||||
- **`src/components/serverpuls/chartColors.js`**: `export const CHART = { line: '#111111', fill: 'rgba(17,17,17,.06)', peak: '#8a93a3', axis: '#5e6678', grid: '#ebeef1', tick: '#d8dde3' }`.
|
||||
|
||||
**CSS-Klassen in `global.css`**
|
||||
|
||||
Prototyp-Namen, Werte aus `quelle/styles.css`:
|
||||
|
||||
| Bereich | Klassen |
|
||||
|---|---|
|
||||
| Gerüst | `.shell(.menu-open)`, `.sidebar`, `.brand`, `.nav`, `.nav-item(.sub .active .paused)`, `.nav-count(.hot)`, `.nav-dot(.critical)`, `.sidebar-foot`, `.main`, `.mobile-bar`, `.scrim`, `.page` |
|
||||
| Seitenkopf | `.crumb`, `.page-head`, `.eyebrow`, `.sub`, `.actions`, `h2.section` |
|
||||
| Knöpfe | `.btn`, `.btn-primary`, `.btn-quiet`, `.btn-block` |
|
||||
| Links | `.link`, `.link-danger` |
|
||||
| Badges | `.badge(.blue .orange .violet .green .red .plain)` |
|
||||
| Reiter | `.tabs`, `.tab(.active)`, `.count` |
|
||||
| Formular | `.toolbar`, `.grow`, `.input`, `.select`, `.textarea`, `.search`, `.select-quiet`, `.check`, `.field`, `.hint`, `.field-row`, `.field-row-3`, `.code-input` |
|
||||
| Tabelle | `.table`, `tr.row`, `.right`, `.cell-title`, `.cell-sub`, `.id`, `.table-foot`, `.hide-md`, `.hide-sm` |
|
||||
| Zustände | `.empty`, `.loading`, `.msg(.err .ok .warn)`, `.toast` |
|
||||
| Werte | `.facts`, `.fact`, `.k`, `.v`, `.stats`, `.stat`, `.v.warn`, `.v.neg`, `.kv`, `.two-col`, `.prose` |
|
||||
| Bedienelemente | `.urgent-toggle(.on)`, `.sw`, `.list-row`, `.seg(button.on)` |
|
||||
| Dialog | `.overlay`, `.dialog`, `.dialog-wide`, `.dialog-head`, `.dialog-close`, `.dialog-body`, `.dialog-foot` |
|
||||
| Login | `.login`, `.login-box`, `.login-links`, `.login-foot` |
|
||||
| Hilfsklassen | `.num`, `.muted`, `.faint`, `.small`, `.xs`, `.strong`, `.nowrap` |
|
||||
| Zustände | `:disabled` (Deckkraft .55) und `[readonly]` (`--bg-subtle`) |
|
||||
|
||||
`.ws*` kommt mit P2, `.week`/`.day`/`.appt` mit P3.
|
||||
|
||||
**Screenshot-Werkzeug aus P0**
|
||||
|
||||
Es liegt unter `$W`.
|
||||
|
||||
| Befehl | Wirkung |
|
||||
|---|---|
|
||||
| `env.sh` | Umgebung, siehe oben |
|
||||
| `check.sh [--nur-tests\|--nur-build]` | Tests und/oder Build mit echtem Exit-Code |
|
||||
| `build.sh $WT <name>` | Vite-Build mit Test-Endpunkten nach `$W/dist/<name>` |
|
||||
| `shots.sh <name> <ausgabe> [--only id,id] [--gate p<N>]` | Screenshots plus Prüfbericht `<ausgabe>/report.json` |
|
||||
| `run-browser.sh <skript.mjs> [args]` | jedes andere Browser-Skript unter Speichergrenze |
|
||||
|
||||
Zu `shots.sh`:
|
||||
- Eine unbekannte ID in `--only` bricht mit Exit 2 ab.
|
||||
- `--gate p<N>` endet bei Verstößen mit Exit 1.
|
||||
|
||||
**`$W/views.mjs`**
|
||||
- Format: `VIEWS: Array<{ id, path, fixtures = 'standard', scenario = 'angemeldet', viewports?: Array<[w, h]>, steps?: async (page, ctx) => void }>`.
|
||||
- `ctx.backend` gibt Zugriff auf `calls` (alle beantworteten Requests) und `violations`.
|
||||
- Ein Fehler in `steps` erzeugt den Verstoß `step-failed`.
|
||||
- Standard-Viewports sind `[[1440, 900], [390, 844]]`.
|
||||
|
||||
**`$W/gates.mjs`**
|
||||
- Format: `GATES: { p<N>: { checks: string[], views: '*' | string[], scoped?: { [check]: { views: string[], ignore?: string[] } } } }`.
|
||||
- `checks` gilt für `views`. Einträge in `views` dürfen mit `*` enden und gelten dann als Präfix (z. B. `'tickets-*'`). Einträge mit führendem `!` schließen aus (nach den Einschlüssen). Ein Muster, das keine Ansicht trifft, eine leere Liste oder ein unbekannter Prüfname beenden den Lauf mit Exit 2.
|
||||
- Immer aktiv sind zusätzlich `unhandled-request`, `backend-error` und `missing-asset` (fehlende statische Datei im Build).
|
||||
- `bad-text` meldet sichtbare Texte wie „Invalid Date“, „NaN“, „undefined“, „[object Object]“ und das Wort „null“. Ab P2 ist die Prüfung im Gate scharf.
|
||||
- Alle Läufe nutzen die feste Uhr `FIXTURE_TODAY=2026-10-05` (überschreibbar per Umgebungsvariable). So sind Bilder verschiedener Tage vergleichbar.
|
||||
- Gate `p1` prüft `overflow-x` mit `['*', '!admin', '!admin-neu']`, weil die Admin-Seite heute schon waagerecht scrollt. P8 behebt das und nimmt die Ausnahme heraus.
|
||||
- Das Werkzeug ist ein eigenes Git-Repo unter `$W`. Wer es ändert, committet dort. Selbsttests: `node selftest.mjs` und `"$W/run-browser.sh" "$W/checks-selftest.mjs"`.
|
||||
- `scoped` schaltet eine weitere Prüfung nur für die genannten Ansichten ein. Beispiel: `icon-svg` für die Ansichten des Pakets, `ignore` mit Selektoren, die bis zu einem späteren Paket ausgenommen bleiben.
|
||||
- `step-failed`, `external-request` und `console-error` sind in jedem Gate aktiv.
|
||||
- Paket N legt `p<N>` als `{ ...GATES['p<N-1>'], scoped: { ...vorherige, 'icon-svg': { views: [… Ansichten von N und allen früheren Paketen] } } }` an.
|
||||
|
||||
**Ansichten-IDs der Kundenseite**
|
||||
- Die Reiter-IDs kommen aus `src/lib/customerTabs.js`: `kunde-overview`, `kunde-tickets`, `kunde-invoices`, `kunde-projects`, `kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`, `kunde-protokoll`.
|
||||
- Pakete verwenden diese IDs und legen keine zweite Ansicht für dieselbe URL an.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Diese Eingaben und Zustände werden von keinem Lib-Test abgedeckt, treffen aber echte Nutzer. Jede Zeile hat eine Prüfung im Paket, das den Code besitzt.
|
||||
|
||||
1. **Altdaten in Tickets:**
|
||||
- Freitext-Datum `2.5.2026`, `31.02.2026` oder ISO-Datum
|
||||
- Ticket ohne `woid`
|
||||
- unbekannter oder leerer Status
|
||||
- fehlender `customerName`
|
||||
|
||||
Erwartet wird, dass die Seite rendert und „–“ oder den Rohtext zeigt. Die Prüfung liegt in P0 (Testdaten) und P2 (Ansicht `ticket-altdaten`).
|
||||
2. **Lange Inhalte am Handy:**
|
||||
- Betreff mit 120 Zeichen
|
||||
- Firmenname mit 60 Zeichen
|
||||
- viele Badges
|
||||
|
||||
Erwartet wird kein waagerechtes Scrollen bei 390 px. Die Prüfung `overflow-x` liegt in P0 und ist ab P1 für alle Ansichten scharf.
|
||||
3. **Teilausfall der Abfragen** (Zähler, Spalte „Zeit“, Mitarbeiterliste antworten mit 500): Die Liste bleibt bedienbar, statt Zahlen steht „–“. Die Prüfung ist das Szenario `fehler` in P0, angewendet in P2.
|
||||
4. **Nur Tastatur:**
|
||||
- Dialog per Esc schließen
|
||||
- Tab bleibt im Dialog
|
||||
- „ד ist bei `busy` gesperrt
|
||||
- Tabellenzeilen per Tab erreichbar
|
||||
|
||||
Die Prüfung liegt in P1 (Dialog) und P2 (Ticketzeilen), jeweils als Tastaturschritte in `views.mjs`.
|
||||
5. **Viele Einträge:**
|
||||
- Ticket mit 300 Arbeitsblättern
|
||||
- Suche mit 250 Treffern
|
||||
|
||||
Erwartet werden: Spalte „Zeit“ nur für die ersten 200 Zeilen mit Hinweis im Fuß, keine falschen Summen, Seite bleibt bedienbar. Die Prüfung ist das Testdaten-Set `viel` in P0, angewendet in P2.
|
||||
@@ -0,0 +1,389 @@
|
||||
# WOMS „Weißes Design“ P0: Vorbereitung – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P0 baut und testet der Worktree sauber, und der tote Code ist weg. Ein Screenshot- und Prüfwerkzeug erzeugt für jede Ansicht Bilder und meldet Design-Verstöße. Dabei wird kein Produktionssystem berührt.
|
||||
|
||||
**Architecture:**
|
||||
- `node_modules` wird aus Git genommen, danach läuft im Worktree ein echtes `npm ci`.
|
||||
- Das Werkzeug liegt außerhalb des Repos unter `$W`.
|
||||
- Es baut die App mit **Vite** gegen erfundene Test-Hosts (`https://ticket.test`, `https://portal.test`). Vite nutzt intern esbuild und liefert dasselbe CSS-Bündel und dieselbe Reihenfolge wie der Produktions-Build. Ein eigenes esbuild-Bündel müsste Vite-Env und JSX-Einstellungen nachbauen. Die Spec nennt „esbuild-Bündel“, gleichwertig ist dieser Weg.
|
||||
- Playwright beantwortet alle Anfragen aus Testdaten und blockiert jeden anderen Host.
|
||||
|
||||
**Tech Stack:**
|
||||
- Vite 5
|
||||
- playwright-core 1.62.1 aus `/root/.npm/_npx/db89d7302a373f10/node_modules/playwright-core`, Laden per `createRequire`
|
||||
- Browser `/root/.cache/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell`
|
||||
- Node 24
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Paket P0 und „Abnahme“. Globale Regeln stehen in `2026-10-05-weisses-design-00-uebersicht.md`. Die Befehlsregeln dort (`env.sh`, `check.sh`, `run-browser.sh`) entstehen in diesem Paket.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P0 kommt hinzu:
|
||||
|
||||
- Das Werkzeug ändert weder `package.json` noch die Lock-Datei noch `tests/` im Repo.
|
||||
- Kein Request darf einen echten Host erreichen. Jeder nicht nachgebildete Request wird abgebrochen und als Verstoß `external-request` gemeldet.
|
||||
- Für die Bilder blendet das Werkzeug die Schrift Inter ein, damit die Breiten realistisch sind. Quelle: `/home/webklar/apps/webklar.com/dev-relaunch/public/fonts/inter-latin.woff2` und `inter-latin-ext.woff2`, gesetzt per `addInitScript` als `--font`. Am Repo ändert sich dadurch nichts.
|
||||
|
||||
## Review Focus
|
||||
|
||||
P0 liefert die Testdaten und Prüfungen für die Übersichtspunkte 1 bis 5:
|
||||
- Altdaten (`altdaten`)
|
||||
- lange Inhalte (`altdaten`, Prüfung `overflow-x`)
|
||||
- Teilausfall (Szenario `fehler`)
|
||||
- Tastatur (Schritte über `steps`)
|
||||
- viele Einträge (`viel`)
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Worktree lauffähig machen
|
||||
|
||||
**Files:**
|
||||
- Modify (Git-Index): `node_modules/**` wird aus Git genommen, nicht gelöscht
|
||||
- Read: `.gitignore` (muss `node_modules` enthalten)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: Im Worktree laufen `npm test` und `npm run build` ohne Symlink.
|
||||
|
||||
- [ ] **Step 1: Startprüfung**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && git fetch origin && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/LeadsPage.jsx src/hooks/useLeads.js && git rebase origin/test && git status --short | grep -v '^ D node_modules' ; grep -n '^node_modules' .gitignore`
|
||||
Expected:
|
||||
- Die `git log`-Zeile für TP2 gibt nichts aus. Gibt sie etwas aus: stoppen und den Eigentümer fragen.
|
||||
- Der Rebase läuft ohne Konflikt.
|
||||
- Vor dem `grep` auf `.gitignore` erscheint keine Zeile.
|
||||
- Danach erscheint eine Zeile `node_modules` oder `node_modules/`.
|
||||
|
||||
- [ ] **Step 2: `node_modules` aus dem Git-Index nehmen**
|
||||
|
||||
Run: `git rm -r -q --cached node_modules && git commit -q -m "node_modules nicht mehr im Repo (Deploy installiert ohnehin neu)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git show --stat HEAD | tail -1`
|
||||
Expected: rund 8890 Löschungen.
|
||||
|
||||
- [ ] **Step 3: Sauber installieren**
|
||||
|
||||
Run: `rm -rf node_modules && systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 npm ci --no-audit --no-fund && git status --short`
|
||||
Expected: Exit 0 und keine Zeile aus `git status`.
|
||||
|
||||
- [ ] **Step 4: Ausgangswerte festhalten**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && T=/root/reports/ticketsystem-design-2026-10-04/werkzeug/tmp && mkdir -p "$T" && npm test > "$T/p0-test.log" 2>&1; echo "exit $?"; grep -E "^# (pass|fail)" "$T/p0-test.log"; systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1200 npm run build 2>&1 | tail -6`
|
||||
|
||||
Task 1 und 2 laufen vor `env.sh` (kommt in Task 3), deshalb der feste Ordner `$T`.
|
||||
|
||||
Expected: `exit 0`, `# pass 312`, `# fail 0` und ein Build ohne Fehler. Testzahl und Bundle-Größen (JS, CSS) kommen in die Commit-Nachricht von Task 2.
|
||||
|
||||
### Task 2: Toten Code löschen
|
||||
|
||||
**Files:**
|
||||
- Delete: `src/pages/LeadsPage.jsx`, `src/hooks/useLeads.js`
|
||||
- Keep: `src/lib/leads.js`, die Route `/leads` mit Umleitung in `src/App.jsx`, den pausierten Menüpunkt in `src/lib/navLinks.js`
|
||||
|
||||
- [ ] **Step 1: Unbenutzt beweisen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && rg -n "LeadsPage|useLeads" src tests`
|
||||
Expected: Treffer nur in den beiden Dateien selbst.
|
||||
|
||||
- [ ] **Step 2: Löschen und prüfen**
|
||||
|
||||
Run: `cd /home/webklar/apps/ticket.webklar.com/dev-design && T=/root/reports/ticketsystem-design-2026-10-04/werkzeug/tmp && git rm -q src/pages/LeadsPage.jsx src/hooks/useLeads.js && npm test > "$T/p0-test.log" 2>&1 && systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1200 npm run build > "$T/p0-build.log" 2>&1 && echo OK`
|
||||
Expected: `OK`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git commit -q -m "LeadsPage und useLeads entfernt (seit Lead-Pause unbenutzt)" -m "Ausgangswerte: <Testzahl>, JS <x> B, CSS <y> B" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: Werkzeug – Umgebung, Build, Test-Backend
|
||||
|
||||
**Files** (alle unter `$W=/root/reports/ticketsystem-design-2026-10-04/werkzeug/`):
|
||||
- Create: `env.sh`, `check.sh`, `run-browser.sh`, `build.sh`
|
||||
- Create: `backend.mjs`, `appwrite-query.mjs`, `endpunkte.md`, `endpunkte.mjs`
|
||||
- Create: `fixtures/standard.mjs`, `fixtures/altdaten.mjs`, `fixtures/viel.mjs`
|
||||
- Create: `README.md` (Bedienung in zehn Zeilen)
|
||||
- Test: `selftest.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- **`env.sh`** setzt `WT`, `W`, `TMPDIR` (Scratchpad, wenn als `$CLAUDE_SCRATCHPAD` bekannt, sonst `$W/tmp`), `set -o pipefail`, `cd "$WT"`.
|
||||
- **`check.sh [--nur-tests|--nur-build]`**
|
||||
- `npm test` schreibt nach `$TMPDIR/test.log`. Ausgegeben werden die Zeilen `# pass`/`# fail`.
|
||||
- Der Build läuft mit `NODE_OPTIONS=--max-old-space-size=1200` unter `systemd-run` (1500M) und schreibt nach `$TMPDIR/build.log`. Ausgegeben werden die Größen der `.js`- und `.css`-Dateien.
|
||||
- Exit ist 0 nur, wenn beide Teile Exit 0 hatten.
|
||||
- **`run-browser.sh <skript.mjs> [args]`** startet `node` unter `systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0`.
|
||||
- **`build.sh <worktree> <name>`**
|
||||
- Baut nach `$W/dist/<name>/` mit `VITE_APPWRITE_ENDPOINT=https://ticket.test/v1`, `VITE_APPWRITE_PROJECT_ID=test` und `VITE_PROJECT_ADMIN_URL=https://portal.test`.
|
||||
- Prozess-Variablen haben in Vite Vorrang vor `.env.production`.
|
||||
- Dazu `--outDir` und dieselbe Speichergrenze wie `check.sh`.
|
||||
- **`createBackend({ fixtures, scenario })`** aus `backend.mjs`.
|
||||
- `scenario` ist `'angemeldet' | 'abgemeldet' | 'mfa' | 'fehler'`.
|
||||
- Rückgabe: `{ handle(route, request), calls: [{ method, url, body }], violations: [] }`.
|
||||
- **`applyQueries(documents, queries) → { total, documents }`** aus `appwrite-query.mjs`.
|
||||
|
||||
- [ ] **Step 0: Umgebungs-Skripte anlegen**
|
||||
|
||||
`env.sh`, `check.sh`, `run-browser.sh` und `build.sh` werden laut Interfaces geschrieben. `chmod +x` gilt für alle außer `env.sh`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && echo "$WT $W $TMPDIR" && "$W/check.sh" --nur-tests; echo "exit $?"`
|
||||
Expected: drei Pfade, die Zeilen `# pass`/`# fail` und `exit 0`.
|
||||
|
||||
- [ ] **Step 1: Endpunkt-Inventar anlegen**
|
||||
|
||||
`endpunkte.mjs` sucht in `$WT/src`:
|
||||
- `fetch(` mit Basis-URL
|
||||
- `VITE_PROJECT_ADMIN_URL`-Basen mit Pfad
|
||||
- `/api/…`-Literale
|
||||
- `databases.*`-Aufrufe mit Collection-Konstanten
|
||||
- `account.*`- und `client.call`-Aufrufe
|
||||
|
||||
Es gibt eine Liste `{ datei, methode, muster }` aus. Daraus entsteht von Hand `endpunkte.md`, eine Tabelle mit diesen Spalten:
|
||||
- Quelldatei
|
||||
- Endpunkt-Muster
|
||||
- Fixture-Schlüssel
|
||||
- Antwortform (Felder, die der Code liest)
|
||||
- Verhalten im Szenario `fehler`
|
||||
|
||||
`fehler` ist so festgelegt:
|
||||
|
||||
| Anfrage | Antwort im Szenario `fehler` |
|
||||
|---|---|
|
||||
| `GET …/collections/worksheets/documents` | 500 |
|
||||
| `GET …/collections/employees/documents` | 500 |
|
||||
| `GET https://portal.test/api/admin/employees` | 500 |
|
||||
| `GET https://portal.test/api/team/*` | 500 |
|
||||
| `GET …/collections/workorders/documents` mit `limit(1)` (Zähler) | 500 |
|
||||
| alle anderen | normal |
|
||||
|
||||
- [ ] **Step 2: Failing Selbsttest schreiben**
|
||||
|
||||
`selftest.mjs` prüft mit `node:assert`:
|
||||
|
||||
```js
|
||||
const { total, documents } = applyQueries(docs, ['equal("status", ["Open","Occupied"])', 'notEqual("type", ["Akquise"])', 'orderDesc("$createdAt")', 'limit(2)'])
|
||||
// total = Treffer vor limit, documents.length === 2, neueste zuerst
|
||||
applyQueries(docs, ['equal("woid", ["100131"])']) // findet woid als Zahl oder Text
|
||||
applyQueries(docs, ['offset(2)', 'limit(1)']) // drittes Dokument
|
||||
applyQueries(docs, ['cursorAfter("id2")', 'limit(1)'])
|
||||
applyQueries(docs, ['select(["woid","totalTime"])']) // nur diese Felder plus $id
|
||||
```
|
||||
|
||||
Weitere Fälle:
|
||||
- Ein Request an `https://example.com/x` wird abgebrochen und steht als `{ kind: 'external-request', url }` in `violations`.
|
||||
- `GET https://ticket.test/v1/account` liefert 200 im Szenario `angemeldet`, 401 in `abgemeldet` und in `mfa` (nach dem Login) 401 mit `type: 'user_more_factors_required'`.
|
||||
- Im Szenario `fehler` liefert `GET …/collections/worksheets/documents` 500.
|
||||
- Jedes Muster aus `endpunkte.md` liefert im Szenario `angemeldet` eine Antwort, nie eine `external-request`-Verletzung. Das Muster wird aus der Tabelle gelesen.
|
||||
|
||||
- [ ] **Step 3: Selbsttest laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cd "$W" && node selftest.mjs`
|
||||
Expected: FAIL (Module fehlen).
|
||||
|
||||
- [ ] **Step 4: `applyQueries` implementieren**
|
||||
|
||||
Unterstützt werden:
|
||||
- Filter: `equal`, `notEqual`, `greaterThan`, `lessThan`, `contains`, `search` (Teilstring, ohne Groß-/Kleinschreibung), `isNull`, `isNotNull`
|
||||
- Sortierung: `orderAsc`, `orderDesc`
|
||||
- Seiten: `limit` (Standard 25), `offset`, `cursorAfter`
|
||||
- Felder: `select`
|
||||
|
||||
Ein unbekannter Query-Typ ergibt den Verstoß `unknown-query` und wird ignoriert.
|
||||
|
||||
- [ ] **Step 5: Backend implementieren**
|
||||
|
||||
| Anfrage | Antwort |
|
||||
|---|---|
|
||||
| `https://ticket.test/v1/...` | Appwrite-Nachbau |
|
||||
| `https://ticket.test/api/...` | gleichseitige APIs (Serverpuls u. a.) laut Inventar |
|
||||
| sonstige `https://ticket.test/...` | Datei aus `$W/dist/<name>/`, unbekannte Pfade bekommen `index.html` |
|
||||
| `https://portal.test/...` | Portal- und Admin-API laut Inventar. Der Chat-Composer (`loadChatComposer.js`) bekommt eine leere JS- und CSS-Datei, bis P6 den echten Baustein einbindet. |
|
||||
| andere URLs | `route.abort()` und Verstoß `external-request` |
|
||||
|
||||
Schreibende Requests ändern die Testdaten im Speicher. Jeder Request landet in `calls`.
|
||||
|
||||
- [ ] **Step 6: Testdaten anlegen**
|
||||
|
||||
**`fixtures/standard.mjs`**
|
||||
- Angemeldet ist `kenso-admin` mit Labels `['admin']`, Name „Kenso Grimm“.
|
||||
- 4 Mitarbeiter: KNSO, ANDJ, JUSN, NIRE.
|
||||
- 9 erfundene Kunden aus `quelle/data.js`:
|
||||
- Stufen fest, lead und archiv
|
||||
- einer mit `customerStatus: 'lost'` (Abgesagt)
|
||||
- einer mit `isImportant: true`
|
||||
- 12 Tickets mit allen Werten aus `TICKET_STATUSES` plus `In Kontakt`, `Zugesagt`, `Abgesagt` und `Assigned`. Arten Webpage, Migration, Project und Akquise.
|
||||
- **Ticket 100131 ist fest:**
|
||||
- Kunde „Physiotherapie am Markt“
|
||||
- `serviceType: 'On Site'`
|
||||
- Arbeitsblätter:
|
||||
- Git-Push ohne Zeit (`serviceType: 'GIT'`, `employeeName: 'knso'`, `totalTime: 0`)
|
||||
- Git-Push mit 45 min (`employeeName: 'ANDJ'`)
|
||||
- Kommentar (`isComment: true`)
|
||||
- Arbeitszeit von `andj` über 60 min mit `supporters: '[{"userId":"kenso-admin","task":"DNS geprüft"}]'`
|
||||
- Arbeitszeit von `kenso-admin` über 90 min mit `countsToPlan: true`
|
||||
- Projekt `WEBklar/physio-am-markt`
|
||||
- 10 `websiteProjects` aller Typen, darunter eine Vorlage und ein archiviertes.
|
||||
- Rechtliches: Dokumente, Versionen und Anbieter.
|
||||
- Antworten für Finanzen, Serverpuls (mit Zeitreihen für die Diagramme), Gutschriften, Abo-Angebote, E-Mail-Speicher und Chat-Liste, jeweils laut Inventar.
|
||||
|
||||
**`fixtures/altdaten.mjs`**
|
||||
- Ticket `startDate: '2.5.2026'`
|
||||
- Ticket `deadline: '31.02.2026'`
|
||||
- Ticket `startDate: '2026-10-07T09:00:00.000Z'`
|
||||
- Ticket ohne `woid`
|
||||
- Ticket mit `status: ''`, Ticket mit `status: 'Completed'`
|
||||
- Ticket ohne `customerName`
|
||||
- Betreff mit 120 Zeichen
|
||||
- Kundenname mit 60 Zeichen
|
||||
|
||||
**`fixtures/viel.mjs`**
|
||||
- Ticket 100131 mit 300 Arbeitsblättern
|
||||
- 250 Tickets, deren Betreff „Test“ enthält
|
||||
|
||||
- [ ] **Step 7: Selbsttest grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cd "$W" && node selftest.mjs`
|
||||
Expected: `selftest ok`.
|
||||
|
||||
- [ ] **Step 8: Probebau**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" vorher && ls "$W/dist/vorher/index.html" && rg -n "https://(ticket|project)\.webklar\.com" "$W"/dist/vorher/assets/*.js | cut -c1-160`
|
||||
Expected: `index.html` ist vorhanden. Treffer der echten Hosts sind nur Texte oder Links für Menschen, keine API-Basen. Jeden Treffer prüfst du einzeln.
|
||||
|
||||
- [ ] **Step 9: `check.sh` vollständig prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh"; echo "exit $?"`
|
||||
Expected: Die Zeilen `# pass 312` und `# fail 0`, die Bundle-Größen und `exit 0`.
|
||||
|
||||
### Task 4: Werkzeug – Ansichten, Prüfungen, Vorher-Bilder
|
||||
|
||||
**Files** (unter `$W`):
|
||||
- Create: `views.mjs`, `checks.mjs`, `gates.mjs`, `shots.mjs`, `shots.sh`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Backend, Testdaten und `build.sh` aus Task 3.
|
||||
- Produces: Formate von `VIEWS` und `GATES` wie in der Übersicht. Zusätzlich:
|
||||
- `runChecks(page) → Promise<Array<{ check, selector, detail }>>` aus `checks.mjs`.
|
||||
- `submitReachable(page)` aus `views.mjs` sucht den Absende-Knopf im offenen Dialog (`[type=submit]`, sonst den letzten Primärknopf), ruft `scrollIntoView` auf und wirft, wenn er danach nicht vollständig im Viewport liegt.
|
||||
- `shots.sh <name> <ausgabe> [--only ids] [--gate p<N>] [--static <ordner>]`
|
||||
- Eine unbekannte ID in `--only` endet mit Exit 2.
|
||||
- Einträge in `views` eines Gates dürfen mit `*` enden (Präfix).
|
||||
- `--static` lädt ein HTML-Verzeichnis ohne Backend. So prüft man das Werkzeug am Prototyp.
|
||||
|
||||
- [ ] **Step 1: Ansichten anlegen**
|
||||
|
||||
| ID | Pfad / Schritte |
|
||||
|---|---|
|
||||
| `login` | `/login`, Szenario `abgemeldet` |
|
||||
| `login-code` | Szenario `mfa`: Formular absenden, warten auf `#mfa-code` |
|
||||
| `login-code-senden` | Szenario `mfa`: Formular absenden, Seite neu laden, warten auf den Knopf „Code senden“ |
|
||||
| `tickets` | `/tickets` |
|
||||
| `tickets-suche` | Testdaten `viel`, `/tickets`, Suche „Test“ (heute über „Anwenden“) |
|
||||
| `ticket` | `/tickets?woid=100131` (heute aufgeklappte Karte) |
|
||||
| `ticket-arbeitsblaetter` | wie `ticket`, dann Reiter „Arbeitsblätter“ |
|
||||
| `ticket-altdaten` | Testdaten `altdaten`, `/tickets` |
|
||||
| `fehler-tickets` | Szenario `fehler`, `/tickets` |
|
||||
| `dlg-ticket-neu` | Dialog „Neues Ticket“ |
|
||||
| `dlg-arbeitsblatt` | Dialog „Arbeitsblatt hinzufügen“ |
|
||||
| `dlg-zeiten` | Dialog „Zeit nachtragen“ |
|
||||
| `dlg-unterstuetzung` | Dialog „Ich habe unterstützt“ |
|
||||
| `planboard` | `/planboard` |
|
||||
| `kunden` | `/customers` |
|
||||
| `kunde-<reiter>` | `/customers/<id>?tab=<reiter>` mit den Reiter-IDs aus der Übersicht |
|
||||
| `chat` | `/customers/chat` |
|
||||
| `gutschriften` | `/customers/gutschriften` |
|
||||
| `angebote` | `/customers/angebote` |
|
||||
| `speicher` | `/customers/email-speicher` |
|
||||
| `projekte` | `/projects` |
|
||||
| `dlg-projekt-neu` | Dialog „Neues Projekt“ |
|
||||
| `dlg-projekt-bearbeiten` | Dialog „Projekt bearbeiten“ |
|
||||
| `dlg-projekt-art` | Art-Frage in `TicketProjects` |
|
||||
| `zuordnen` | `/projects/zuordnen/<id>` |
|
||||
| `services` | `/services` |
|
||||
| `analyse-zustimmung` | Zustimmungsablauf in `AnalyseRow` |
|
||||
| `finanzen` | `/finance` |
|
||||
| `dlg-rechnung` | Rechnungsdialog |
|
||||
| `dlg-zuordnen-zahlung` | `AssignDialog` |
|
||||
| `finanzen-einstellungen` | `/finance/einstellungen` |
|
||||
| `rechtliches` | `/rechtliches` |
|
||||
| `rechtliches-anbieter` | `/rechtliches`, zweiter Reiter |
|
||||
| `dlg-vertrag` | Vertrags-Formular |
|
||||
| `dlg-version` | Versions-Formular |
|
||||
| `admin` | `/admin` |
|
||||
| `admin-neu` | Mitarbeiter-Formular offen |
|
||||
| `serverpuls` | `/serverpuls`, warten bis `canvas` gezeichnet ist |
|
||||
|
||||
Alle `dlg-*`-Ansichten:
|
||||
- haben `viewports: [[1440, 900], [390, 844], [1440, 700], [390, 700]]`
|
||||
- rufen am Ende `submitReachable(page)` auf
|
||||
|
||||
Jede Ansicht wartet auf `document.fonts.ready` und 300 ms. Dazu kommt `reducedMotion: 'reduce'`.
|
||||
|
||||
- [ ] **Step 2: Prüfungen schreiben**
|
||||
|
||||
`checks.mjs` läuft im Browser über alle sichtbaren Elemente:
|
||||
|
||||
| Prüfung | Regel |
|
||||
|---|---|
|
||||
| `dark-surface` | Verstoß bei eigenem, deckendem Hintergrund mit relativer Helligkeit < 0,5. Für `background-image` mit `linear-gradient` zählt der Mittelwert der Farbstopps. Ausnahmen: `.btn-primary`, `.badge`, `.toast`, `.nav-dot`, `.day.today .dn`, `.sw`, `.code-input`, `[class*="lightbox"]`, `[class*="camera"]` und `canvas`. |
|
||||
| `low-contrast` | Verstoß bei Textkontrast < 3:1 gegen den nächsten deckenden Hintergrund. Gradienten zählen wie oben. Ausnahmen: `disabled`, `aria-disabled` und Platzhalter. |
|
||||
| `icon-svg` | Verstoß bei einem sichtbaren `svg` außerhalb von `.brand`, `.login-box > svg` und `.woms-chat .chat-composer *` |
|
||||
| `overflow-x` | Verstoß bei `scrollWidth > innerWidth + 1` |
|
||||
| `console-error` | Verstoß bei `pageerror` oder `console.error`. Im Szenario `fehler` sind die erwarteten 500-Meldungen ausgenommen. |
|
||||
| `external-request`, `unknown-query` | Verstöße aus dem Backend |
|
||||
| `step-failed` | eine Ausnahme in `steps` |
|
||||
|
||||
- [ ] **Step 3: Gates festlegen**
|
||||
|
||||
```js
|
||||
export const GATES = {
|
||||
p0: { checks: ['external-request', 'console-error', 'step-failed'], views: '*' },
|
||||
p1: { checks: ['external-request', 'console-error', 'step-failed', 'dark-surface', 'low-contrast', 'overflow-x'], views: '*' },
|
||||
}
|
||||
```
|
||||
|
||||
Spätere Pakete ergänzen nach dem Schema der Übersicht.
|
||||
|
||||
- [ ] **Step 4: Läufer schreiben**
|
||||
|
||||
`shots.mjs`:
|
||||
- Je Ansicht und Viewport ein Kontext. Unter 1000 px Breite gilt `isMobile`, `hasTouch` und `deviceScaleFactor: 2`.
|
||||
- Dazu `locale: 'de-DE'` und `timezoneId: 'Europe/Berlin'`.
|
||||
- Ausgabe: `<ausgabe>/<w>x<h>/<id>.png` und `<ausgabe>/report.json` mit `{ view, viewport, issues[] }`.
|
||||
- Der Exit-Code richtet sich nach `--gate`.
|
||||
|
||||
`shots.sh` startet `shots.mjs` über `run-browser.sh`.
|
||||
|
||||
- [ ] **Step 5: Vorher-Bilder**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" vorher "$W/shots/vorher" --gate p0`
|
||||
Expected:
|
||||
- Exit 0.
|
||||
- Für jede Ansicht ein Bild je Viewport.
|
||||
- Kein `external-request`.
|
||||
- `dark-surface` und `low-contrast` dürfen massenhaft auftreten. Sie sind nicht gate-relevant.
|
||||
|
||||
- [ ] **Step 6: Gegenprobe am Prototyp**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" prototyp "$W/shots/prototyp" --static /root/reports/ticketsystem-design-2026-10-04/quelle --only tickets`
|
||||
Expected:
|
||||
- 0 Verstöße für `dark-surface` und `low-contrast`.
|
||||
- Zusätzlich hat `$W/shots/vorher/report.json` für `tickets` mindestens einen `dark-surface`-Treffer. Das beweist, dass die Prüfung anschlägt.
|
||||
|
||||
### Task 5: Abschluss P0
|
||||
|
||||
- [ ] **Step 1: Tag**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh" && git tag design-p0`
|
||||
Expected: Exit 0 und das Tag ist gesetzt. P0 wird nicht gepusht, sondern geht mit P1 live.
|
||||
|
||||
- [ ] **Step 2: Code-Review (sonnet)**
|
||||
|
||||
Geprüft werden `603d08c..design-p0` und das Werkzeug (`$W`), mit Blick auf zwei Fragen:
|
||||
- Kann ein Request einen echten Host erreichen?
|
||||
- Melden die Prüfungen falsch-negativ?
|
||||
|
||||
Wichtige Befunde werden behoben. Ein späterer Commit bekommt das Tag `design-p0-fix1`.
|
||||
|
||||
- [ ] **Step 3: Bericht an den Eigentümer**
|
||||
|
||||
Der Bericht nennt die Ausgangswerte, den Ort der Vorher-Bilder und den Hinweis, dass P0 unsichtbar ist und mit P1 live geht.
|
||||
@@ -0,0 +1,751 @@
|
||||
# WOMS „Weißes Design“ P1: Fundament – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P1 ist das ganze Ticketsystem weiß und lesbar:
|
||||
- Das Menü ist eine Textliste.
|
||||
- Das Login ist neu.
|
||||
- Alle Dialoge sind weiß.
|
||||
- Die neuen Bausteine stehen für P2 bis P9 bereit.
|
||||
|
||||
Die Innenlayouts der Seiten bleiben vorerst alt.
|
||||
|
||||
**Architecture:** `src/styles/global.css` bekommt fünf Blöcke:
|
||||
|
||||
1. Tokens und Aliase
|
||||
2. Basis (Reset, Hilfsklassen, Links)
|
||||
3. Bausteine (Prototyp-Klassen)
|
||||
4. KOMPAT (Altklassen im neuen Look, bis P10)
|
||||
5. Bereichs-CSS (Finanzen, Rechtliches), bis auf Pflicht-Fixes unverändert
|
||||
|
||||
Gleichnamige Altdefinitionen werden vor dem Einfügen der Bausteine gelöscht. Die Komponenten unter `src/components/ui/` behalten ihre Schnittstellen. Neu kommen `Dialog`, `Stat`, `Logo` und `statusTone` hinzu.
|
||||
|
||||
**Tech Stack:** React 18, react-router-dom 6, Vite 5, reines CSS. Tests mit `node --test`, Screenshots mit dem Werkzeug aus P0.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Design-Tokens“, „Bausteine“, „Übergangsschicht KOMPAT“ und Paket P1. Die Prototyp-Werte stehen in `/root/reports/ticketsystem-design-2026-10-04/quelle/styles.css`. Globale Regeln, Befehlsregeln (`env.sh`, `check.sh`) und gemeinsame Schnittstellen stehen in `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P1 kommt hinzu:
|
||||
|
||||
- Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT`, `$W` und `$TMPDIR`.
|
||||
- Werte für Tokens, Abstände, Größen und Schatten werden aus der Spec bzw. aus `quelle/styles.css` übernommen und nicht neu erfunden.
|
||||
- Nicht übernommen werden `html[data-accent]`, `.proto*` und `.nav-gap`.
|
||||
- Verhalten, Routen und Auth bleiben unverändert. Ausnahmen:
|
||||
- Der Footer entfällt.
|
||||
- Das Einklappen entfällt.
|
||||
- Die Schublade unter 860 px kommt neu dazu.
|
||||
- Seiten in `src/pages/*` werden nur dort angefasst, wo dieser Plan es nennt:
|
||||
- `LoginPage`
|
||||
- `ProjectsPage`: Wurzelklasse
|
||||
- `AdminPage`: Wurzelelement
|
||||
- `ServerpulsPage`: Diagrammfarben
|
||||
- Die Spec erlaubt keine neue `robots`-Meta-Zeile. `index.html` bekommt nur das Favicon.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- **Übersichtspunkt 2 (lange Inhalte am Handy):** `overflow-x` ist im Gate `p1` für alle Ansichten scharf.
|
||||
- **Übersichtspunkt 4 (Tastatur):** Die Esc- und Sperrlogik des Dialogs prüft ein Lib-Test in Task 7.
|
||||
- **Altseiten:** Jede Altseite bleibt mit KOMPAT lesbar. Das prüft das Gate `p1` mit `dark-surface` und `low-contrast`, auch für Verläufe.
|
||||
- **Alt-Dialoge:** Die zehn Alt-Dialoge müssen bei 390×700 und 1440×700 bis zum Speichern-Knopf erreichbar sein. Das prüft `submitReachable` in jeder `dlg-*`-Ansicht.
|
||||
|
||||
---
|
||||
|
||||
### Task 0: Paketstart
|
||||
|
||||
- [ ] **Step 1: Stand prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p0 HEAD && git log --oneline origin/test..feature/terminplaner-tp2 -- src/styles src/components/ui src/App.jsx src/components/Navbar.jsx src/pages/LoginPage.jsx src/lib/navLinks.js src/lib/ticketStatus.js && git rebase origin/test && "$W/check.sh"`
|
||||
Expected:
|
||||
- Exit 0.
|
||||
- Die TP2-Zeile gibt nichts aus. Gibt sie etwas aus: stoppen und den Eigentümer fragen. Empfehlung: TP2 zuerst nach `test` bringen.
|
||||
|
||||
### Task 1: `statusTone`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/ticketStatus.js` (neuer Export nach `statusGroup`)
|
||||
- Test: `tests/unit/ticketStatus.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `statusGroup(status)` aus derselben Datei. Es liefert die Gruppen-ID oder `null`.
|
||||
- Produces: `statusTone(status) → '' | 'blue' | 'orange' | 'violet' | 'green' | 'plain'`.
|
||||
|
||||
- [ ] **Step 1: Failing Test**
|
||||
|
||||
```js
|
||||
const { statusTone } = await loadModule('src/lib/ticketStatus.js')
|
||||
test('statusTone: Ton je gespeichertem Wert', () => {
|
||||
for (const s of ['Open', '', undefined, null, 'Completed']) assert.equal(statusTone(s), '')
|
||||
for (const s of ['Occupied', 'Assigned', 'Added Info', 'In Kontakt']) assert.equal(statusTone(s), 'blue')
|
||||
for (const s of ['Awaiting', 'Halted']) assert.equal(statusTone(s), 'orange')
|
||||
assert.equal(statusTone('In Test'), 'violet')
|
||||
for (const s of ['Closed', 'Zugesagt']) assert.equal(statusTone(s), 'green')
|
||||
for (const s of ['Cancelled', 'Aborted', 'Abgesagt']) assert.equal(statusTone(s), 'plain')
|
||||
})
|
||||
```
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/ticketStatus.test.mjs`
|
||||
Expected: FAIL, `statusTone is not a function`.
|
||||
|
||||
- [ ] **Step 2: Implementieren**
|
||||
|
||||
Zuordnung über `statusGroup`:
|
||||
|
||||
| Gruppe | Ton |
|
||||
|---|---|
|
||||
| `open` | `''` |
|
||||
| `working` | `'blue'` |
|
||||
| `waiting` | `'orange'` |
|
||||
| `review` | `'violet'` |
|
||||
| `done` | `'green'` |
|
||||
| `cancelled` | `'plain'` |
|
||||
| `null` | `''` |
|
||||
|
||||
`statusClass` bleibt bis P2.
|
||||
|
||||
- [ ] **Step 3: Grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/ticketStatus.test.mjs && "$W/check.sh" --nur-tests`
|
||||
Expected: PASS, Exit 0.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/ticketStatus.js tests/unit/ticketStatus.test.mjs
|
||||
git commit -m "Ticketstatus: statusTone fuer die neuen Badge-Toene" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 2: Tokens, Aliase, Basis, Schrift
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css`
|
||||
- Zeilen 1-10 (`@font-face`) löschen
|
||||
- `:root`, alte Zeilen 18-71, ersetzen
|
||||
- `body`/Scrollbar/`::selection`, alte Zeilen 73-106, ersetzen
|
||||
- alte Hilfsregeln `.muted`/`.faint` (um Zeile 1051) ersetzen
|
||||
- Modify: `src/pages/AdminPage.css` (Zeilen 2-3, `--admin-line*`, löschen)
|
||||
- Delete: `src/assets/fonts/lato-regular.ttf`, `src/assets/fonts/OFL-Lato.txt`
|
||||
- Modify (Werkzeug): `$W/views.mjs` bekommt die Ansicht `basis-weiss`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: alle Tokens und Aliase der Spec, dazu die Basis- und Hilfsklassen aus der Übersicht.
|
||||
|
||||
- [ ] **Step 1: Prüf-Ansicht (failing)**
|
||||
|
||||
`basis-weiss` öffnet `/tickets`. Der Schritt prüft, dass `getComputedStyle(document.body).backgroundColor === 'rgb(255, 255, 255)'` und dass `font-family` mit `-apple-system` beginnt.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t2" --only basis-weiss --gate p0`
|
||||
Expected: Exit 1 mit `step-failed`.
|
||||
|
||||
- [ ] **Step 2: `:root` ersetzen**
|
||||
|
||||
- Den Token-Block der Spec wörtlich übernehmen.
|
||||
- Darunter folgt die Alias-Tabelle der Spec. Sie enthält `--admin-line` und `--admin-line-strong`.
|
||||
- Die beiden Zeilen in `AdminPage.css` entfallen.
|
||||
|
||||
- [ ] **Step 3: Basis setzen**
|
||||
|
||||
Aus `quelle/styles.css` Zeilen 63-91 übernehmen. Zusätzlich gilt:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `html` | `color-scheme: light` |
|
||||
| `body` | `font-family: var(--font); font-size: 14px; line-height: 20px; color: var(--text); background: var(--bg); -webkit-font-smoothing: antialiased`, ohne `overflow: hidden` |
|
||||
| Formularelemente | `button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer }` und `input, select, textarea { font: inherit; color: inherit }` |
|
||||
| `a` | `color: inherit; text-decoration: none` |
|
||||
| `a:not([class])` | Unterstrich in `--line-strong`, `text-underline-offset: 3px`, beim Überfahren `text-decoration-color: currentColor` |
|
||||
| Hilfsklassen | `.num` (`tabular-nums`), `.muted` (`--text-2`), `.faint` (`--text-3`), `.small` 13px, `.xs` 12/16, `.strong` 600, `.nowrap` |
|
||||
| `.link`, `.link:hover`, `.link-danger`, `.link-danger:hover` | Werte aus dem Prototyp |
|
||||
| `:focus-visible` | `outline: 2px solid #111111; outline-offset: 2px` |
|
||||
| `:disabled` | `opacity: .55; cursor: not-allowed` |
|
||||
| `[readonly]` | `background: var(--bg-subtle)` |
|
||||
|
||||
- [ ] **Step 4: Schrift entfernen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/assets/fonts/lato-regular.ttf src/assets/fonts/OFL-Lato.txt && rg -n "lato|Lato" src`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 5: Grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t2" --only basis-weiss --gate p0`
|
||||
Expected: Exit 0.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/pages/AdminPage.css
|
||||
git commit -m "Design: weisse Tokens, Aliase, Basis und Hilfsklassen, Systemschrift" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: Bausteine-CSS
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css`: Altdefinitionen gleichnamiger Klassen löschen, Block „Bausteine“ hinter der Basis anlegen
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: alle Klassen aus der Übersicht („CSS-Klassen in `global.css`“).
|
||||
|
||||
- [ ] **Step 1: Gleichnamige Altdefinitionen löschen**
|
||||
|
||||
Gelöscht werden die Regeln dieser Selektoren samt ihrer Varianten (Hover, Kinder, Media-Queries). Die Zeilenangaben beziehen sich auf den Stand 603d08c, gesucht wird nach dem Selektor:
|
||||
|
||||
| Selektor | Zeilen (603d08c) |
|
||||
|---|---|
|
||||
| `.btn` und `.btn:hover`, ohne `.btn-dark`/`.btn-teal`/`.btn-green` (die gehen nach KOMPAT bzw. sterben in Task 8) | ab 188 |
|
||||
| `.table`, `.table th`, `.table td`, `.table td[rowspan]` | 236-275 |
|
||||
| `.overlay` und `.overlay-close` (Neufassung in KOMPAT, Task 4) | ab 563 |
|
||||
| `.page` | 867 |
|
||||
| `.badge` (Basis) | 909-915 |
|
||||
| `.urgent-toggle` | 971-976 |
|
||||
| `.list-row` | 997-1005 |
|
||||
| `.toolbar` | 1018 |
|
||||
| `.seg` | 1019-1024 |
|
||||
| `.empty` | 1028 |
|
||||
| `.field` | 1032 |
|
||||
|
||||
Der übrige alte Block „REDESIGN“ (796-1049) bleibt bis Task 4. Dazu gehören `.ui-card*`, `.ui-btn*`, `.badge-*`, `.kpi*`, `.tcard*`, `.ui-table`, `.list`, `.ticket-tabs`/`.ticket-tab*` und die Hilfsregeln `.mt-*`, `.flex*`, `.gap-*`, `.grow`, `.wrap` und `.text-ellipsis`.
|
||||
|
||||
- [ ] **Step 2: Bausteine übernehmen**
|
||||
|
||||
Aus `quelle/styles.css` werden diese Abschnitte übernommen:
|
||||
- „Grundgerüst“ ohne `.nav-gap`
|
||||
- „Seitenkopf“
|
||||
- „Knöpfe“
|
||||
- „Status-Marken“
|
||||
- „Reiter“
|
||||
- „Filterzeile“
|
||||
- „Tabellen“
|
||||
- „Kennzahlen / Schlüsselwerte“
|
||||
- „Ticket-Kopf“
|
||||
- „Listen“
|
||||
- „Dialog“
|
||||
- „Anmeldung“
|
||||
|
||||
Dazu kommen die Media-Queries 1100 und 860. **Ausgeschlossen** sind dort `.proto*`, `.ws*`, `.week`, `.day*` und `.appt` (sie kommen mit P2 und P3) sowie `html[data-accent]`.
|
||||
|
||||
Zusätzlich:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.page section + section` | `margin-top: 36px` |
|
||||
| `.nav-dot` | `background: var(--nav-dot)` |
|
||||
| `.nav-dot.critical` | `background: var(--danger)` |
|
||||
| `.dialog-wide` | `max-width: 760px` |
|
||||
| `.msg` | 13px |
|
||||
| `.msg.err` | Farbe `--red-fg` |
|
||||
| `.msg.ok` | Farbe `--green-fg` |
|
||||
| `.msg.warn` | Farbe `--orange-fg` |
|
||||
| `.loading` | `padding: 56px 0; text-align: center; color: var(--text-2)` |
|
||||
| `.v.neg` | Farbe `--red-fg` |
|
||||
| `.stat .sub` | `font-size: 12px; line-height: 16px; color: var(--text-2)` |
|
||||
| `.badge.plain` | Farbe `--text-2` |
|
||||
| `.tab.active`, `.tab[aria-selected="true"]` | aktiver Reiter |
|
||||
| `.seg button.on`, `.seg button[aria-pressed="true"]` | aktiver Teil |
|
||||
| `.autocomplete-list` | `background: var(--bg); box-shadow: var(--shadow-pop); border-radius: 8px` |
|
||||
| `.autocomplete-item.active`, `.autocomplete-item:hover` | `background: var(--bg-hover)` |
|
||||
| `.datetime-pair` | zwei Felder nebeneinander, Abstand 8px, kein `color-scheme: dark` |
|
||||
| `[aria-invalid="true"]` und `.is-invalid` an `.input`, `.select`, `.textarea`, `.form-control` | `box-shadow: 0 0 0 1px var(--danger), 0 0 0 4px var(--danger-ring)` |
|
||||
|
||||
- [ ] **Step 3: Jede Basisregel genau einmal**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && for s in btn table overlay page badge urgent-toggle list-row toolbar seg empty field; do printf '%s ' "$s"; rg -c "^\.$s *\{" src/styles/global.css; done`
|
||||
Expected: Hinter jedem Namen steht `1`.
|
||||
|
||||
- [ ] **Step 4: Bauen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
|
||||
Expected: Exit 0.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css
|
||||
git commit -m "Design: Bausteine aus dem Prototyp, gleichnamige Altregeln ersetzt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 4: KOMPAT und tote Klassen
|
||||
|
||||
**Files:**
|
||||
- Create: `$W/css-klassen.mjs`
|
||||
- Create: `docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md`
|
||||
- Modify: `src/styles/global.css` (Block „KOMPAT“, Legacy löschen)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- Der KOMPAT-Block nach der Spec-Tabelle „Übergangsschicht KOMPAT“. P10 entfernt ihn.
|
||||
- `css-klassen.mjs <worktree> [--ohne-ausnahmen]` gibt JSON aus: `{ defined, used, dead, undefinedUsed }`.
|
||||
|
||||
- [ ] **Step 1: Skript schreiben**
|
||||
|
||||
„Benutzt“ ist eine Klasse, wenn eine dieser Bedingungen zutrifft:
|
||||
- Sie steht als Wort in einem `className`-Literal oder Template-String in `src/**/*.jsx`.
|
||||
- Sie steht in einer Zeichenkette in `src/lib/*.js` oder `src/hooks/*.js`.
|
||||
- Ein Template-Präfix deckt sie ab: `xxx-${…}` gilt für `xxx-*`.
|
||||
- Sie steht in der Ausnahmeliste: `fin-neg`, `fin-fixed-off`, `legal-tone-*`, `legal-version-*`, `badge-*`, `status-*`, `ticket-tab-active`, `ui-btn-*`, `side-dot-*`, `woms-chat`, `chat-*`. Mit `--ohne-ausnahmen` entfällt diese Liste.
|
||||
|
||||
„Definiert“ ist eine Klasse, wenn sie in einem Selektor in `global.css`, `chat.css`, `serverpuls.css`, `admin-areas.css` oder `pages/AdminPage.css` steht.
|
||||
|
||||
- [ ] **Step 2: Liste erzeugen und gegenprüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" > "$TMPDIR/klassen.json"`
|
||||
|
||||
Danach für jeden Kandidaten in `dead`: `rg -w <name> src --glob '!*.css'`. Ein Kandidat gilt nur bei 0 Treffern als tot. Die bestätigte Liste kommt mit Klassenname und definierender Datei:Zeile in `2026-10-05-weisses-design-p1-tote-klassen.md`.
|
||||
|
||||
Expected: etwa 66 Namen. Liegt die Zahl unter 40 oder über 90, wird die Abweichung dort begründet.
|
||||
|
||||
- [ ] **Step 3: Legacy löschen, KOMPAT anlegen**
|
||||
|
||||
Gelöscht werden:
|
||||
- die Regeln der bestätigten toten Klassen
|
||||
- alle verbliebenen `!important`
|
||||
- `.side*` außer `.side-count` und `.side-logo` (die gehen nach KOMPAT)
|
||||
- `.footer`, `.navbar*`, `.ticket-row*`, `.worksheet-expansion*`, `.slider`, `.drop-zone`, `.modern-card`
|
||||
- die Legacy-Responsive-Regeln (alt 789-794)
|
||||
- die Keyframes `slideDown`, `fadeIn` und `slideIn`
|
||||
- `.kpi*`, weil `Kpi` ab Task 6 `Stat` rendert
|
||||
|
||||
Danach wird KOMPAT laut Spec-Tabelle angelegt, markiert mit `/* ===== KOMPAT (bis P10) ===== */`. Der Block enthält auch `.page .page` und `.page .main-content`, die Abstand und Breite neutralisieren.
|
||||
|
||||
- [ ] **Step 4: Keine benutzte Klasse ohne Regel**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.join('\n'))"`
|
||||
Expected: Nur diese Namen tauchen auf:
|
||||
- `p-4`, `btn-sm`, `status-filter`, `supporters-editor`, `timeline`
|
||||
- die Bootstrap-Namen aus `WorksheetList.jsx` (heute schon wirkungslos)
|
||||
|
||||
- [ ] **Step 5: Altseiten ansehen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t4" --gate p1; echo "exit $?"`
|
||||
Expected: `step-failed` (`submitReachable`) ist auf keiner `dlg-*`-Ansicht mehr gemeldet. Erlaubt sind nur noch Treffer `dark-surface` und `low-contrast` in Dateien aus Task 9.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md
|
||||
git commit -m "Design: KOMPAT-Schicht fuer Altklassen, tote Regeln und !important entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 5: Gerüst, Menü, Logo, Favicon
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/Logo.jsx`, `public/favicon.svg` (Kopie von `quelle/favicon.svg`)
|
||||
- Modify:
|
||||
- `src/App.jsx`
|
||||
- `src/components/Navbar.jsx`
|
||||
- `src/lib/navLinks.js` (`hot: true` am Chat-Unterpunkt)
|
||||
- `index.html` (nur `<link rel="icon" href="/favicon.svg" type="image/svg+xml">`)
|
||||
- `src/styles/serverpuls.css` (Zeilen 70-74 `.side-dot*` löschen)
|
||||
- `src/pages/ProjectsPage.jsx` (Wurzel `main-content` → `page`)
|
||||
- `src/pages/AdminPage.jsx` (`<main className="page admin-page">` → `<div className="page admin-page">`, zwei Stellen)
|
||||
- Delete: `src/components/Footer.jsx`, `src/lib/sidebar.js`, `tests/unit/sidebar.test.mjs`
|
||||
- Test: `tests/unit/navLinks.test.mjs`
|
||||
- Modify (Werkzeug): `$W/views.mjs` bekommt die Ansicht `menue-mobil`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Klassen aus Task 3.
|
||||
- Produces:
|
||||
- `Logo({ size = 24 })` mit `aria-hidden="true"`. Die Pfade kommen aus `quelle/app.js`, Konstante `LOGO`.
|
||||
- `Sidebar({ onNavigate })`.
|
||||
|
||||
- [ ] **Step 1: Test für `hot` (failing)**
|
||||
|
||||
In `tests/unit/navLinks.test.mjs` die Zeile `assert.equal(kunden.children.find((c) => c.key === 'chat').hot, true)` ergänzen. Die bestehenden `deepEqual` bleiben.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/navLinks.test.mjs`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 2: `navLinks.js` ergänzen, Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/navLinks.test.mjs`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Logo und Favicon anlegen**
|
||||
|
||||
- [ ] **Step 4: Gerüst in `App.jsx`**
|
||||
|
||||
`AppContent` hält `menuOpen` und baut dieses Gerüst:
|
||||
|
||||
```jsx
|
||||
<div className={`shell${menuOpen ? ' menu-open' : ''}`}>
|
||||
<Sidebar onNavigate={close} />
|
||||
<div className="scrim" onClick={close} />
|
||||
<div className="main">
|
||||
<div className="mobile-bar">
|
||||
<Link className="brand" to="/tickets"><Logo />WEBklar</Link>
|
||||
<button type="button" className="btn btn-quiet" onClick={open} aria-expanded={menuOpen} aria-controls="menue">Menü</button>
|
||||
</div>
|
||||
<main id="inhalt"><AppRoutes /></main>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- Ein Effekt auf `location.pathname` (nur darauf) schließt die Schublade und ruft `window.scrollTo(0, 0)` auf.
|
||||
- Ein Esc-Listener ist nur bei `menuOpen` aktiv.
|
||||
- Der Ladezustand von `ProtectedRoute` ist `<div className="loading">Wird geladen …</div>`.
|
||||
|
||||
- [ ] **Step 5: `Navbar.jsx` als Textliste**
|
||||
|
||||
Aufbau: `<aside className="sidebar" id="menue">`, darin `.brand` (Logo und „WEBklar“), `<nav className="nav">` mit `.nav-item` und `.sub` und am Ende `.sidebar-foot`.
|
||||
|
||||
| Element | Markup |
|
||||
|---|---|
|
||||
| Pausiert | `<span className="nav-item paused" aria-disabled="true">Leads<span className="nav-count">pausiert</span></span>` |
|
||||
| Zähler | `<span className={`nav-count${child.hot ? ' hot' : ''}`}>` |
|
||||
| Serverpuls | `<span className={`nav-dot${dot === 'critical' ? ' critical' : ''}`} data-testid="sp-nav-dot" title=…>`, nur wenn `dot` nicht `'ok'` ist. Titeltexte wie heute. |
|
||||
| Fuß | `<span className="who text-ellipsis">{user.name \|\| user.email}</span>` und `<button type="button" className="link" onClick={handleLogout}>Abmelden</button>` |
|
||||
|
||||
Klicks auf Einträge rufen `onNavigate()` auf.
|
||||
|
||||
Entfernt werden:
|
||||
- `@tabler`-Imports und `ICONS`
|
||||
- Einklappen und `startsCollapsed`
|
||||
|
||||
Bleiben:
|
||||
- `isActive` und `isLinkActive`
|
||||
- das Serverpuls-Polling
|
||||
- `useSidebarCounts`
|
||||
|
||||
- [ ] **Step 6: Altdateien löschen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/components/Footer.jsx src/lib/sidebar.js tests/unit/sidebar.test.mjs && rg -n "Footer|startsCollapsed|lib/sidebar" src tests`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 7a: Gate `p1` um die Menü-Prüfung ergänzen**
|
||||
|
||||
In `$W/gates.mjs` bekommt `p1` zusätzlich `scoped['icon-svg'] = { views: ['*'], ignore: ['#inhalt *'] }`. Der vorhandene Eintrag `scoped['overflow-x']` bleibt. Damit prüft `icon-svg` ab hier Menü, Mobil-Leiste und Login auf allen Ansichten, den Seiteninhalt erst im jeweiligen Paket. Commit im Werkzeug-Repo.
|
||||
|
||||
- [ ] **Step 7: Ansicht `menue-mobil`**
|
||||
|
||||
Bei 390×844 läuft:
|
||||
1. Klick auf „Menü“: `.shell.menu-open` ist da.
|
||||
2. Screenshot.
|
||||
3. Esc: `.menu-open` ist weg.
|
||||
4. Menü erneut öffnen, Klick auf „Projekte“: Der Pfad ist `/projects`, und das Menü ist zu.
|
||||
|
||||
- [ ] **Step 8: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t5" --only tickets,kunden,serverpuls,menue-mobil --gate p1`
|
||||
Expected:
|
||||
- Exit 0.
|
||||
- Im Menü ist kein `svg` außer dem Logo.
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add src/App.jsx src/components/Navbar.jsx src/components/Logo.jsx src/lib/navLinks.js tests/unit/navLinks.test.mjs index.html public/favicon.svg src/styles/serverpuls.css src/pages/ProjectsPage.jsx src/pages/AdminPage.jsx
|
||||
git commit -m "Design: Geruest mit Textmenue, Schublade fuers Handy, Logo; Footer und Einklappen entfallen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: UI-Bausteine
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/ui/tones.js`, `src/components/ui/Stat.jsx`
|
||||
- Modify:
|
||||
- `Button.jsx`, `Badge.jsx`, `Card.jsx`, `PageHeader.jsx`, `Tabs.jsx`, `EmptyState.jsx`, `Field.jsx`, `Kpi.jsx`, `StatusPill.jsx`, `index.js`
|
||||
- nur Klasse `form-control` → `input` bzw. `select`: `AutocompleteInput.jsx`, `DateTimeInput.jsx`, `DurationSelect.jsx`
|
||||
- Test: `tests/unit/uiTones.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- die Signaturen aus der Übersicht
|
||||
- `badgeToneClass(tone) → string`
|
||||
- `BUTTON_CLASSES`
|
||||
|
||||
Beide liegen in `tones.js`. `Button` setzt keinen eigenen Standard-`type`.
|
||||
|
||||
- [ ] **Step 1: Failing Test**
|
||||
|
||||
```js
|
||||
const { badgeToneClass, BUTTON_CLASSES } = await loadModule('src/components/ui/tones.js')
|
||||
test('badgeToneClass', () => {
|
||||
for (const t of ['muted', 'grey', undefined, 'unbekannt']) assert.equal(badgeToneClass(t), 'badge')
|
||||
assert.equal(badgeToneClass('info'), 'badge blue')
|
||||
assert.equal(badgeToneClass('blue'), 'badge blue')
|
||||
assert.equal(badgeToneClass('ok'), 'badge green')
|
||||
assert.equal(badgeToneClass('warn'), 'badge orange')
|
||||
assert.equal(badgeToneClass('danger'), 'badge red')
|
||||
assert.equal(badgeToneClass('violet'), 'badge violet')
|
||||
assert.equal(badgeToneClass('plain'), 'badge plain')
|
||||
})
|
||||
test('Button-Varianten', () => {
|
||||
assert.deepEqual(BUTTON_CLASSES, { default: 'btn', ghost: 'btn', primary: 'btn btn-primary', quiet: 'btn btn-quiet', danger: 'link-danger' })
|
||||
})
|
||||
```
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/uiTones.test.mjs`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 2: `tones.js` anlegen, Test grün**
|
||||
|
||||
- [ ] **Step 3: Komponenten umschreiben**
|
||||
|
||||
| Komponente | Änderung |
|
||||
|---|---|
|
||||
| `Card` | ohne Titel und ohne Aktionen nur `<section>` |
|
||||
| `PageHeader` | Brotkrumen als `Link`, wenn `to` gesetzt ist. Das letzte Element ist Text, das „/“ steht in `<span className="faint">`. |
|
||||
| `Tabs` | `active` und `aria-selected` |
|
||||
| `EmptyState` | ohne Inline-Styles |
|
||||
| `Field` | Hinweis als `.hint` |
|
||||
| `Kpi` | gibt `Stat` zurück |
|
||||
| `StatusPill` | ohne `pillBase` |
|
||||
|
||||
- [ ] **Step 4: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t6" --only tickets,ticket,kunde-overview,finanzen --gate p1`
|
||||
Expected: Exit 0. Die Status-Badges in `tickets` zeigen die neuen Töne.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/ui tests/unit/uiTones.test.mjs
|
||||
git commit -m "Design: UI-Bausteine rendern die neuen Klassen (Button, Badge, Card, Kopf, Reiter, Stat)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: `Dialog`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/ui/Dialog.jsx`, `src/lib/dialogKeys.js`
|
||||
- Modify: `src/components/ui/index.js`, `$W/views.mjs` (Ansicht `dlg-probe`)
|
||||
- Test: `tests/unit/dialogKeys.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `useDialogFocus(dialogRef)` aus `src/hooks/useDialogFocus.js`, unverändert.
|
||||
- Produces:
|
||||
- `Dialog` laut Übersicht
|
||||
- `shouldCloseOnKey({ key, defaultPrevented }, busy) → boolean`
|
||||
|
||||
- [ ] **Step 1: Failing Test**
|
||||
|
||||
```js
|
||||
const { shouldCloseOnKey } = await loadModule('src/lib/dialogKeys.js')
|
||||
test('Esc schliesst nur ohne Sperre und ohne vorherige Behandlung', () => {
|
||||
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: false }, false), true)
|
||||
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: true }, false), false)
|
||||
assert.equal(shouldCloseOnKey({ key: 'Escape', defaultPrevented: false }, true), false)
|
||||
assert.equal(shouldCloseOnKey({ key: 'Enter', defaultPrevented: false }, false), false)
|
||||
})
|
||||
```
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/dialogKeys.test.mjs`
|
||||
Expected: FAIL.
|
||||
|
||||
- [ ] **Step 2: Implementieren, Test grün**
|
||||
|
||||
- [ ] **Step 3: `Dialog.jsx`**
|
||||
|
||||
| Teil | Umsetzung |
|
||||
|---|---|
|
||||
| Esc | `keydown`-Listener auf `document`, prüft mit `shouldCloseOnKey(e, busy)` und ruft dann `onClose()` auf |
|
||||
| Hintergrund | Klick ohne Wirkung |
|
||||
| Box | `role="dialog"`, `aria-modal="true"`, `aria-labelledby={titleId}` (`useId()`), `ref`, `tabIndex={-1}` |
|
||||
| Overlay | `onKeyDown` aus `useDialogFocus` |
|
||||
| `×` | `<button type="button" className="dialog-close" aria-label="Schließen" disabled={busy} onClick={onClose}>×</button>` |
|
||||
| `as="form"` | die Box ist `<form onSubmit>` ohne `noValidate` |
|
||||
|
||||
- [ ] **Step 4: `dlg-probe`**
|
||||
|
||||
Die Ansicht öffnet `/tickets` und fügt per `page.evaluate` einen statischen Klon des Dialog-Markups ein: ein Titel, drei Felder und ein Fuß mit Absende-Knopf. Geprüft wird nur das CSS:
|
||||
- Bei 390 px liegt die Box unten.
|
||||
- Das Overlay hat Fenstergröße.
|
||||
- `submitReachable` gelingt bei 390×700.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" p1 "$W/shots/p1-t7" --only dlg-probe --gate p1`
|
||||
Expected: Exit 0.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/ui/Dialog.jsx src/components/ui/index.js src/lib/dialogKeys.js tests/unit/dialogKeys.test.mjs
|
||||
git commit -m "Design: Dialog-Baustein (Esc, Sperre waehrend Speichern, Fokus, Bottom-Sheet)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: Login
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/LoginPage.jsx` (nur Markup)
|
||||
- Delete: `src/components/LetterGlitch.jsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes:
|
||||
- `useAuth()` mit `login`, `mfaPending`, `sendCode`, `verifyCode` und `cancelMfa`, unverändert
|
||||
- `Logo`
|
||||
- die Klassen `.login*`, `.field`, `.input`, `.code-input`, `.btn-primary.btn-block` und `.msg`
|
||||
|
||||
- [ ] **Step 1: Markup ersetzen**
|
||||
|
||||
Grundaufbau: `.login > .login-box > <Logo size={40} />`.
|
||||
|
||||
**Schritt E-Mail und Passwort**
|
||||
|
||||
| Element | Inhalt |
|
||||
|---|---|
|
||||
| Überschrift | h1 „Anmelden“ |
|
||||
| Unterzeile | `.sub` „Ticketsystem für das WEBklar-Team“ |
|
||||
| Feld E-Mail | Label „E-Mail“, `type="email"`, `autoComplete="username"`, Platzhalter `deine@email.com` |
|
||||
| Feld Passwort | Label „Passwort“, `type="password"`, `autoComplete="current-password"`, `minLength={8}` |
|
||||
| Knopf | `.btn.btn-primary.btn-block` „Weiter“, beim Laden „Login läuft...“ |
|
||||
| Fehler | `.msg.err` mit `role="alert"` |
|
||||
|
||||
**Schritt Code**
|
||||
|
||||
| Element | Inhalt |
|
||||
|---|---|
|
||||
| Überschrift | h1 „Code eingeben“ |
|
||||
| Text | die beiden Bestandssätze wörtlich |
|
||||
| Hinweis | `notice` als `.msg.ok` |
|
||||
| Feld | `#mfa-code` mit `className="input code-input"`, Label „Code aus der E-Mail“ |
|
||||
| Knopf | „Anmelden“ bzw. „Prüfe…“ |
|
||||
| Links | `.login-links` mit zwei `button.link` (`type="button"`): „Code erneut senden“ und „Abbrechen“ |
|
||||
|
||||
Ohne `challengeId` erscheint stattdessen „Code senden“ bzw. „Sende…“ und darunter „Abbrechen“.
|
||||
|
||||
**Fuß:** `.login-foot` „Webklar Ticket-Plattform · © {Jahr}“.
|
||||
|
||||
**Bleibt unverändert:** alle Handler und Zustände.
|
||||
|
||||
- [ ] **Step 2: `LetterGlitch` löschen und prüfen**
|
||||
|
||||
Zusätzlich entfällt im Werkzeug die Ausnahme `knownPageError` für LetterGlitch in `$W/shots.mjs` (eigener Commit im Werkzeug-Repo).
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/components/LetterGlitch.jsx && ! rg -n "LetterGlitch" src && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1-t8" --only login,login-code,login-code-senden --gate p1`
|
||||
Expected: Exit 0. Im Bild `login-code-senden` steht der Knopf „Code senden“.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/LoginPage.jsx
|
||||
git commit -m "Design: neues Login (weiss, Logo, Code-Schritte), Glitch-Hintergrund entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 9: Pflicht-Fixes gegen Unlesbares
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/serverpuls/chartColors.js`, `$W/farben.mjs`
|
||||
- Modify:
|
||||
- CSS: `src/styles/chat.css`, `src/pages/AdminPage.css`, `src/styles/admin-areas.css`, `src/styles/serverpuls.css`, `src/styles/global.css` (Bereich Finanzen und Rechtliches)
|
||||
- Projekt-Bausteine: `src/components/AssignedProjectCard.jsx` (Zeilen 27, 82, 102), `src/components/PreviewBlock.jsx`, `src/components/PreviewLinkButton.jsx`, `src/components/GiteaLinkButton.jsx`, `src/components/AnalyseRow.jsx` (`COLORS`)
|
||||
- Arbeitsblätter: `src/components/CreateWorksheetModal.jsx` (Zeilen 194, 262), `src/components/WorksheetTimesModal.jsx` (Zeile 143), `src/components/WorksheetList.jsx` (nur Farben, siehe Step 4)
|
||||
- Serverpuls: `src/components/serverpuls/Chart.jsx`, `src/pages/ServerpulsPage.jsx` (Serienfarben)
|
||||
- Doku: `docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md` (Abschnitt „Bewusst offen“)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `CHART` laut Übersicht.
|
||||
- `farben.mjs <worktree>` listet je Treffer Datei:Zeile, Literal und Grund:
|
||||
- `kontrast<3` (Text auf Weiß)
|
||||
- `dunkel-auf-akzent`
|
||||
- `weiss` (`#fff`, `#ffffff`, `white`, `rgba(255,255,255,x)` als Text- oder Randfarbe außerhalb von `.btn-primary` und Elementen auf `--accent`)
|
||||
- `verlauf` (`linear-gradient` in JSX oder CSS)
|
||||
|
||||
- [ ] **Step 1: Scan (failing)**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/farben.mjs" "$WT"`
|
||||
Expected: eine Liste mit Treffern aller vier Gründe, unter anderem aus `WorksheetList.jsx`.
|
||||
|
||||
- [ ] **Step 2: Chat-Variablen**
|
||||
|
||||
Unter `.woms-chat` in `chat.css`:
|
||||
- `--chat-accent: var(--accent)`
|
||||
- `--chat-accent-hover: var(--accent-hover)`
|
||||
- `--chat-accent-soft: var(--accent-soft)`
|
||||
- `--chat-accent-line: rgba(17,17,17,.14)`
|
||||
- `--chat-accent-text: var(--accent)`
|
||||
- `--chat-on-accent: #ffffff`
|
||||
- `--chat-surface: var(--bg)`
|
||||
- `--chat-line: var(--line-strong)`
|
||||
- `--chat-muted: var(--text-2)`
|
||||
|
||||
Weitere Regeln:
|
||||
- Rand der eigenen Blase: `var(--line)`.
|
||||
- `.woms-chat .chat-messages { background: var(--bg) }`.
|
||||
- `.woms-chat .chat-composer-note`, `.woms-chat .chat-tray-item.is-error .chat-file-size` und `.woms-chat .chat-audio.is-error .chat-audio-time` bekommen `color: var(--red-fg)`.
|
||||
- `.woms-chat .chat-composer-note.is-info` bekommt `color: var(--text-2)`.
|
||||
|
||||
- [ ] **Step 3: Literale umstellen nach fester Zuordnung**
|
||||
|
||||
| Literal | Token |
|
||||
|---|---|
|
||||
| `#a0aec0`, `#cbd5e0`, `#9aa7b4`, `#e2e8f0` (als Text) | `var(--text-2)` |
|
||||
| `#34d399`, `#6ee7b7`, `#10b981` (als Text) | `var(--green-fg)` |
|
||||
| `#f87171`, `#fca5a5`, `#fecaca`, `#ef4444` (als Text) | `var(--red-fg)` |
|
||||
| `#fbbf24`, `#f59e0b` (als Text) | `var(--orange-fg)` |
|
||||
| `#818cf8`, `#9d3dfe` | `var(--violet-fg)` |
|
||||
| `#3b82f6`, `#60a5fa` | `var(--blue-fg)` |
|
||||
| `rgba(255,255,255,x)` als Rand oder Linie | `var(--line)` |
|
||||
| `rgba(255,255,255,x)` als Fläche | entfällt bzw. wird `var(--bg-subtle)` |
|
||||
| `#05251c`, `#04130d` auf Akzent | `#ffffff` |
|
||||
| `rgba(30,41,59,x)` und ähnliche dunkle Flächen | `var(--bg-subtle)` |
|
||||
|
||||
`.admin-primary-button` bekommt `color: #fff` und beim Überfahren `background: var(--accent-hover)`.
|
||||
|
||||
- [ ] **Step 4: `WorksheetList.jsx` minimal**
|
||||
|
||||
Nur die Farben werden geändert:
|
||||
- Verläufe und Weiß-Alphas gehen auf Tokens nach der Tabelle oben.
|
||||
- Weiße Schrift auf farbigen Köpfen wird `var(--text)` auf `var(--bg-subtle)`.
|
||||
|
||||
Struktur und Klassen bleiben. Den Umbau macht P2.
|
||||
|
||||
- [ ] **Step 5: Diagrammfarben**
|
||||
|
||||
`chartColors.js` wird angelegt. `Chart.jsx` nutzt `CHART.axis`, `CHART.grid` (nur horizontal), `CHART.tick` und als Standard-Serienfarbe `CHART.line`. `ServerpulsPage` setzt:
|
||||
- alle Serien `CHART.line`
|
||||
- Spitze `CHART.peak` mit `dash: [4, 4]`
|
||||
- Füllung `CHART.fill`
|
||||
|
||||
- [ ] **Step 6: Scan erneut**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/farben.mjs" "$WT"`
|
||||
Expected:
|
||||
- Kein Treffer `kontrast<3`, `dunkel-auf-akzent` oder `weiss`.
|
||||
- `verlauf`-Treffer nur dort, wo `.btn-primary` oder ein Canvas sie braucht.
|
||||
- Jeder Rest steht unter „Bewusst offen“ im Dokument mit Zielpaket und gemessenem Kontrast von mindestens 3:1.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/serverpuls/chartColors.js src/components/serverpuls/Chart.jsx src/pages/ServerpulsPage.jsx src/styles/chat.css src/pages/AdminPage.css src/styles/admin-areas.css src/styles/serverpuls.css src/styles/global.css src/components/AssignedProjectCard.jsx src/components/PreviewBlock.jsx src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/AnalyseRow.jsx src/components/CreateWorksheetModal.jsx src/components/WorksheetTimesModal.jsx src/components/WorksheetList.jsx docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md
|
||||
git commit -m "Design: Pflicht-Fixes gegen helle Schrift und dunkle Flaechen, Diagrammfarben, Chat-Variablen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 10: Gate P1, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0. Die Testzahl ist 312 − 3 (`sidebar`) + 1 (`statusTone`) + 2 (`uiTones`) + 1 (`dialogKeys`) = 313. Liegen neue Commits auf `test`, kann sie höher sein; die Zahl wird notiert.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p1 && "$W/shots.sh" p1 "$W/shots/p1" --gate p1`
|
||||
Expected: Exit 0 für alle Ansichten aus P0 plus `basis-weiss`, `menue-mobil` und `dlg-probe`. Alle `dlg-*` bestehen `submitReachable` bei 700 px Höhe.
|
||||
|
||||
- [ ] **Step 3: Bundle-Größe**
|
||||
|
||||
Die Werte aus der `check.sh`-Ausgabe werden mit P0 verglichen. Das CSS sollte kleiner sein, und die Schrift fehlt.
|
||||
|
||||
- [ ] **Step 4: Code-Review (sonnet)**
|
||||
|
||||
Der Diff `603d08c..HEAD` (inklusive P0) wird gegen die Spec geprüft, Abschnitte:
|
||||
- „Gestaltungsregeln“
|
||||
- „Bausteine“
|
||||
- „KOMPAT“
|
||||
- Pakete P0 und P1
|
||||
|
||||
Wichtige Befunde werden behoben. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only tickets,login`.
|
||||
|
||||
- [ ] **Step 5: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p1 && git log --oneline origin/test..design-p1 && git rev-parse design-p1`
|
||||
|
||||
Übergabe an den Eigentümer:
|
||||
- Screenshots aus `$W/shots/p1` (Login, Tickets, Kunde, Projekte, Finanzen, Serverpuls, ein Alt-Dialog, Menü mobil)
|
||||
- Hinweis zu den neuen Statusfarben für das Team
|
||||
- Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p1^{commit}:refs/heads/test'`
|
||||
- Rücknahme: `git revert --no-commit 603d08c..design-p1`, Commit und Push durch den Eigentümer
|
||||
|
||||
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` stimmt mit `dist/index.html` des Tags überein.
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers: Login-Seite und Ticketliste.
|
||||
@@ -0,0 +1,197 @@
|
||||
# P1 Task 4: tote Klassen und Übergangsschicht KOMPAT
|
||||
|
||||
Ergebnis des Klassen-Abgleichs mit `css-klassen.mjs` (Werkzeug-Repo `$W`). Zeilenangaben beziehen sich auf `global.css` im Stand `a6877aa` (vor diesem Commit). Spec: Abschnitt „Übergangsschicht KOMPAT“.
|
||||
|
||||
## Werkzeug
|
||||
|
||||
`node "$W/css-klassen.mjs" "$WT" [--ohne-ausnahmen] [--mit-neu]` gibt JSON aus: `{ defined, used, dead, undefinedUsed }` und dazu `definedAt` (Name → Datei:Zeile), `prefixes`, `ausnahmen`, `laufzeit`, `neuOhneBenutzer`, `counts`.
|
||||
|
||||
- **Definiert:** Klasse in einem Selektor in `global.css`, `chat.css`, `serverpuls.css`, `admin-areas.css` oder `pages/AdminPage.css` (Regeln in `@media` und `@container` zählen, `@keyframes` nicht). Geparst mit `postcss`.
|
||||
- **Benutzt:** Wort in einem `className`-Ausdruck in `src/**/*.jsx` (Literale, Template-Strings, Zweige von `?:`, rechte Seite von `&&`, Teile einer `+`-Kette; Bedingungen und Vergleiche wie `filter === 'all'` zählen nicht; ein Ergebnis, das an Text klebt wie `fin-todo-${x || 'info'}`, ist nur ein Namensteil). Dazu Wörter aus Zeichenketten in `src/lib/*.js` und `src/hooks/*.js`, die eine definierte Klasse sind. Geparst mit `@babel/parser`.
|
||||
- **Präfixe:** `xxx-${…}` oder `'xxx-' + …` deckt `xxx-*`.
|
||||
- **Ausnahmeliste** (entfällt mit `--ohne-ausnahmen`): `fin-neg`, `fin-fixed-off`, `legal-tone-*`, `legal-version-*`, `badge-*`, `status-*`, `ticket-tab-active`, `ui-btn-*`, `side-dot-*`, `woms-chat`, `chat-*`.
|
||||
- **NEU-Bereiche:** Regeln zwischen den Kommentaren `NEU-ANFANG` und `NEU-ENDE` in `global.css` (Basis, Hilfsklassen und Bausteine aus Task 2 und 3) sind das neue System; ihre Benutzer entstehen in P1 Task 5 bis 8 und in P2 bis P9. Eine Klasse mit einer Regel dort gilt nie als tot und steht in `neuOhneBenutzer` (mit `--mit-neu` zählt sie normal, so findet P10 unbenutzt gebliebene neue Klassen). Ohne diese Trennung stünden 77 neue Klassen als „tot“ in der Liste.
|
||||
- **Laufzeit-Klassen** (gelten immer als benutzt): `u-*` (uPlot erzeugt `u-legend` im DOM) und `mine` (Modifikator der Sprechblasen des Chat-Bausteins aus dem Kundenportal, gesetzt über `renderMessage(…, { mine })` in `ChatThread.jsx`).
|
||||
|
||||
## Ergebnis
|
||||
|
||||
| Lauf | definiert | tot |
|
||||
|---|---|---|
|
||||
| Vor P1 (`bd84d22`, ohne NEU-Bereiche) | 514 | **67** |
|
||||
| Vor diesem Task (`a6877aa`), roh | 594 | 144 |
|
||||
| davon Laufzeit-Klassen (`mine`, `u-legend`) | | 2 |
|
||||
| davon NEU-Bereiche (Task 2 und 3, noch ohne Benutzer) | | 77 |
|
||||
| **Kandidaten (Altlast)** | | **65** |
|
||||
| Nach diesem Task | 524 | 3 (siehe Gegenprobe) |
|
||||
|
||||
Erwartet waren etwa 66 Namen (Grenzen 40 und 90). Der Lauf auf dem Stand vor P1 ergibt 67, die Altlast-Kandidaten im aktuellen Stand 65: Die Differenz sind `link-danger`, `num`, `nowrap`, `small`, `strong` und `xs` (jetzt NEU) sowie `mine` und `u-legend` (Laufzeit), dazu kommen `stat-label` und `stat-value` wie unten.
|
||||
|
||||
## Gegenprobe
|
||||
|
||||
Für jeden der 65 Kandidaten: `rg -w <name> src --glob '!*.css'`. Bei Treffern zusätzlich mit genauer Wortgrenze (`rg -e '(^|[^A-Za-z0-9_-])<name>($|[^A-Za-z0-9_-])' --glob '!*.css' src`), weil `-w` den Bindestrich als Wortgrenze behandelt und `stat-value` in `fin-stat-value` „findet“.
|
||||
|
||||
- 62 Kandidaten: 0 Treffer.
|
||||
- `stat-label`, `stat-value`: je 3 Treffer, alle in `fin-stat-label` und `fin-stat-value` (`AboCard.jsx`); mit genauer Wortgrenze 0, also tot.
|
||||
- `success`: 71 Treffer, wirklich benutzt: `AdminPage.jsx` Zeile 25 setzt `admin-notice ${notice.type}` (`success` oder `error`). Bleibt.
|
||||
- `admin-status`, `admin-empty-line`: 0 Treffer, tot, aber in `pages/AdminPage.css`, das nicht zu diesem Paket gehört (Task 9 und P8 fassen die Datei an). Bleiben vorerst.
|
||||
|
||||
Gelöscht sind damit 62 Namen aus `global.css`.
|
||||
|
||||
| Klasse | Definition (Stand `a6877aa`) | `rg -w` Treffer | Ergebnis |
|
||||
|---|---|---|---|
|
||||
| `admin-empty-line` | pages/AdminPage.css:361, pages/AdminPage.css:372 | 0 | tot, bleibt vorerst: Datei liegt ausserhalb dieses Pakets (P8 baut die Admin-Seite um) |
|
||||
| `admin-status` | pages/AdminPage.css:280, pages/AdminPage.css:293 | 0 | tot, bleibt vorerst: Datei liegt ausserhalb dieses Pakets (P8 baut die Admin-Seite um) |
|
||||
| `bg-amber` | global.css:494 | 0 | gelöscht |
|
||||
| `bg-black` | global.css:487 | 0 | gelöscht |
|
||||
| `bg-blue` | global.css:490 | 0 | gelöscht |
|
||||
| `bg-blue-grey` | global.css:491 | 0 | gelöscht |
|
||||
| `bg-dark-grey` | global.css:484, global.css:748, global.css:761 | 0 | gelöscht |
|
||||
| `bg-green` | global.css:488 | 0 | gelöscht |
|
||||
| `bg-light-grey` | global.css:485 | 0 | gelöscht |
|
||||
| `bg-orange` | global.css:495 | 0 | gelöscht |
|
||||
| `bg-teal` | global.css:489 | 0 | gelöscht |
|
||||
| `bg-white` | global.css:486 | 0 | gelöscht |
|
||||
| `bg-yellow` | global.css:493 | 0 | gelöscht |
|
||||
| `btn-secondary` | global.css:593 | 0 | gelöscht |
|
||||
| `col-12` | global.css:695 | 0 | gelöscht |
|
||||
| `container` | global.css:504 | 0 | gelöscht |
|
||||
| `control-panel-section` | global.css:1009, global.css:1014 | 0 | gelöscht |
|
||||
| `dashboard-section` | global.css:1238 | 0 | gelöscht |
|
||||
| `drop-zone` | global.css:963, global.css:972 | 0 | gelöscht |
|
||||
| `fade-in` | global.css:1041 | 0 | gelöscht |
|
||||
| `flex-col` | global.css:1321 | 0 | gelöscht |
|
||||
| `form-select` | global.css:638, global.css:654, global.css:1308, global.css:1309 | 0 | gelöscht |
|
||||
| `gap-1` | global.css:1324 | 0 | gelöscht |
|
||||
| `gap-4` | global.css:1324 | 0 | gelöscht |
|
||||
| `mb-1` | global.css:707 | 0 | gelöscht |
|
||||
| `modal-content` | global.css:904 | 0 | gelöscht |
|
||||
| `modern-card` | global.css:993, global.css:1003, global.css:1306, global.css:1307 | 0 | gelöscht |
|
||||
| `mt-1` | global.css:704 | 0 | gelöscht |
|
||||
| `nav-link` | global.css:545, global.css:553 | 0 | gelöscht |
|
||||
| `nav-right` | global.css:558 | 0 | gelöscht |
|
||||
| `navbar` | global.css:518 | 0 | gelöscht |
|
||||
| `navbar-brand` | global.css:531 | 0 | gelöscht |
|
||||
| `navbar-nav` | global.css:540, global.css:1095 | 0 | gelöscht |
|
||||
| `priority-high` | global.css:838 | 0 | gelöscht |
|
||||
| `priority-low` | global.css:836 | 0 | gelöscht |
|
||||
| `priority-medium` | global.css:837 | 0 | gelöscht |
|
||||
| `priority-none` | global.css:835 | 0 | gelöscht |
|
||||
| `quick-actions` | global.css:1239 | 0 | gelöscht |
|
||||
| `side-group-label` | global.css:1134, global.css:1138 | 0 | gelöscht |
|
||||
| `slide-in` | global.css:1045 | 0 | gelöscht |
|
||||
| `slider` | global.css:916, global.css:926, global.css:930, global.css:941, global.css:946, global.css:957 | 0 | gelöscht |
|
||||
| `slider-container` | global.css:911 | 0 | gelöscht |
|
||||
| `spacer` | global.css:609 | 0 | gelöscht |
|
||||
| `stat-card` | global.css:1234 | 0 | gelöscht |
|
||||
| `stat-info` | global.css:1235 | 0 | gelöscht |
|
||||
| `stat-label` | global.css:1237 | 3, alle nur Teil von `fin-stat-label` | gelöscht |
|
||||
| `stat-value` | global.css:1236 | 3, alle nur Teil von `fin-stat-value` | gelöscht |
|
||||
| `stats-grid` | global.css:1233 | 0 | gelöscht |
|
||||
| `success` | pages/AdminPage.css:99 | 71 | bleibt: dynamischer Modifikator `admin-notice ${notice.type}` (AdminPage.jsx:25, Typen `success`/`error`) |
|
||||
| `table-hover` | global.css:600, global.css:604 | 0 | gelöscht |
|
||||
| `text-black` | global.css:498 | 0 | gelöscht |
|
||||
| `text-green` | global.css:500 | 0 | gelöscht |
|
||||
| `text-large` | global.css:701 | 0 | gelöscht |
|
||||
| `text-right` | global.css:699 | 0 | gelöscht |
|
||||
| `text-small` | global.css:700 | 0 | gelöscht |
|
||||
| `text-xlarge` | global.css:702 | 0 | gelöscht |
|
||||
| `ticket-collapsed` | global.css:742, global.css:748 | 0 | gelöscht |
|
||||
| `ticket-expanded` | global.css:755, global.css:761 | 0 | gelöscht |
|
||||
| `ticket-id` | global.css:742, global.css:755, global.css:794, global.css:807 | 0 | gelöscht |
|
||||
| `ticket-id-expanded` | global.css:807, global.css:811 | 0 | gelöscht |
|
||||
| `ticket-info` | global.css:820 | 0 | gelöscht |
|
||||
| `ticket-row` | global.css:715, global.css:720, global.css:727, global.css:731, global.css:735 | 0 | gelöscht |
|
||||
| `ticket-time` | global.css:815 | 0 | gelöscht |
|
||||
| `worksheet-cell` | global.css:771, global.css:777 | 0 | gelöscht |
|
||||
| `worksheet-expansion` | global.css:767 | 0 | gelöscht |
|
||||
|
||||
## Nicht gelöscht, obwohl der Abgleich sie roh nennt
|
||||
|
||||
- **NEU (77, Task 2 und 3, Benutzer folgen):** `actions`, `brand`, `btn-block`, `btn-primary`, `btn-quiet`, `cell-sub`, `cell-title`, `check`, `code-input`, `count`, `critical`, `crumb`, `dialog`, `dialog-body`, `dialog-close`, `dialog-foot`, `dialog-head`, `dialog-wide`, `err`, `eyebrow`, `fact`, `facts`, `field-row`, `field-row-3`, `hide-md`, `hide-sm`, `hint`, `hot`, `input`, `is-invalid`, `k`, `kv`, `link-danger`, `loading`, `login`, `login-box`, `login-foot`, `login-links`, `main`, `menu-open`, `mobile-bar`, `msg`, `nav`, `nav-count`, `nav-dot`, `nav-item`, `neg`, `nowrap`, `num`, `page-head`, `paused`, `prose`, `right`, `scrim`, `search`, `section`, `select`, `select-quiet`, `shell`, `sidebar`, `sidebar-foot`, `small`, `stat`, `stats`, `strong`, `sub`, `sw`, `tab`, `table`, `table-foot`, `tabs`, `textarea`, `toast`, `two-col`, `v`, `who`, `xs`
|
||||
- **Laufzeit (2):** `mine`, `u-legend`.
|
||||
- **Nur durch die Ausnahmeliste am Leben** (Lauf mit `--ohne-ausnahmen`, 6 Namen): `chat-bubble`, `chat-input-row`, `chat-meta` (Sprechblasen und Eingabezeile kommen zur Laufzeit aus dem Chat-Baustein des Portals), `ui-btn-danger`, `ui-btn-sm` (`Button.jsx` setzt sie über die Tabelle `VARIANTS` und `size`; beide stehen in KOMPAT). `status-occupied` hat keinen Benutzer: `statusClass()` in `lib/ticketStatus.js` liefert nur `status-open`, `-working`, `-awaiting`, `-assigned` und `-closed`. Es ist gelöscht, die übrigen fünf stehen in KOMPAT.
|
||||
|
||||
## Zusätzlich gelöscht (nicht tot, laut Plan)
|
||||
|
||||
- Alle `!important` in `global.css`: 77 vorher, 0 nachher. Offen bleiben 6 in `pages/AdminPage.css` und 2 in `admin-areas.css` (beide Dateien gehören nicht zu diesem Paket; Task 9 und P8).
|
||||
- `.btn-green, .btn-primary { … !important }` samt Hover und `.btn-secondary`; `.btn-dark`, `.btn-teal`, `.btn-green` stehen neu in KOMPAT.
|
||||
- `.footer` (zweimal), `.navbar*`, `.nav-link`, `.nav-right`, `.ticket-row*`, `.ticket-collapsed`, `.ticket-expanded`, `.ticket-id*`, `.ticket-time`, `.ticket-info`, `.worksheet-expansion`, `.worksheet-cell`, `.slider*`, `.drop-zone`, `.modern-card`.
|
||||
- Die Legacy-Responsive-Regeln (`@media (max-width: 768px)` mit `.col-6`, `.navbar-nav`, `.ticket-tab`); `.col-6 { width: 100% }` unter 768 px steht neu in KOMPAT.
|
||||
- Die Keyframes `slideDown`, `fadeIn` und `slideIn` (`WorksheetList.jsx` bringt eigene in einem `<style>`-Block mit).
|
||||
- `.kpi*` einschließlich `.kpi-grid`: `Kpi` rendert ab Task 6 `Stat`; bis dahin hat `Kpi` kein Styling. `.fin-stack .kpi-grid` und `.fin-head .kpi-value` im Finanz-Teil bleiben (Task 9).
|
||||
- `.priority-none`, `-low`, `-medium`, `-high` (tot); `.priority-critical` und `.status-*` stehen in KOMPAT.
|
||||
- `.ui-card*`, `.ui-btn*`, `.badge-*`, `.form-group`, `.form-label`, `.form-control` (und `.form-select`, tot), `.card*`, `.row`, `.col`, `.col-6`, `.dropdown*`, `.spinner`, `.overlay-content`, `.ticket-tab*` und die Regeln „Soften legacy“: durch KOMPAT-Zeilen ersetzt.
|
||||
- `.fin-form-field .form-control[aria-invalid="true"] { border-color: … !important }`: ersetzt durch die roten Ringe aus Task 3 (`.form-control[aria-invalid="true"]`).
|
||||
- Die Aliasvariable `--sidebar-w-collapsed` (Task 2).
|
||||
- Die toten Hilfsklassen `.flex-col`, `.gap-1`, `.gap-4`, `.container`, `.mt-1`, `.mb-1`, `.text-right`, `.text-small`, `.text-large`, `.text-xlarge`, `.col-12`, `.bg-*` außer `.bg-red`, `.text-black`, `.text-green` (stehen in der Tabelle oben).
|
||||
|
||||
### Abweichung: `.side*` bleibt bis Task 5
|
||||
|
||||
Der Plan nennt `.side*` zum Löschen. Das alte Menü (`Navbar.jsx`) benutzt sie aber bis Task 5. Ohne sie fällt das Alt-Gerüst `.app-shell` zusammen: Der Lauf `shots.sh --gate p1` brach auf fast allen Ansichten mit `step-failed` ab, weil das ungebremste Menü den Inhalt verdrängte (auf dem Handy auch ohne die Regel für `.collapsed`). Die Regeln stehen deshalb unverändert in einem eigenen Teil des KOMPAT-Blocks („Altes Menü“). Geändert sind nur `--sidebar-w-collapsed` (jetzt `72px`) und das tote `.side-group-label` (entfernt). **Task 5 löscht diesen Teil**; dafür muss `global.css` in die Dateiliste von Task 5. `.side-count` und `.side-logo` stehen wie im Plan als eigene KOMPAT-Zeilen.
|
||||
|
||||
## KOMPAT-Zeilen (Selektor → Look)
|
||||
|
||||
| Selektor | Look |
|
||||
|---|---|
|
||||
| `.ui-card`, `.ui-card.pad`, `.ui-card-head`, `.ui-card-head h3`, `.ui-card-body` | Abschnitt ohne Kasten, Kopf wie `h2.section` (16/24, 600) |
|
||||
| `.ui-btn`, `.ui-btn-primary`, `.ui-btn-danger` | wie `.btn` (weiß, Ring, 32 px), `.btn-primary` (schwarz), `.link-danger` (roter Textlink); `-ghost` und `-sm` ohne eigene Regel (wirken wie `.ui-btn`) |
|
||||
| `.btn-dark`, `.btn-teal`, `.btn-green` | wie `.btn-primary` |
|
||||
| `.form-group`, `.form-label`, `.form-control` (mit `textarea`, `select`, `:focus`, `[readonly]`, Platzhalter) | wie `.field`, Beschriftung 500, `.input`/`.select`/`.textarea` |
|
||||
| `.badge-muted`, `-info`, `-warn`, `-ok`, `-danger` | Badge-Töne grau, blau, orange, grün, rot |
|
||||
| `.status-open`, `-working`, `-awaiting`, `-assigned`, `-closed`, `.priority-critical` | grau, blau, orange, violett, „plain“ (geschlossen und storniert teilen sich die Klasse), rot |
|
||||
| `.overlay:has(> .overlay-content)` | `overflow-y: auto`, `align-items: flex-start`, Abstand 24/16, Hintergrund `--overlay`, ohne Einblend-Animation |
|
||||
| `.overlay-content` (und `.overlay-content.fin-dialog`) | weiße Box: 100 %, max. 520 px (760 px bei `.fin-dialog`), Radius 12, `--shadow-pop`, Abstand 24, Schrift `--text` |
|
||||
| `.overlay-close` | fest oben rechts, 32×32, weißer runder Knopf, „ד in `--text-2` |
|
||||
| `.seg button.active` | wie `.seg button.on` |
|
||||
| `.bg-red` (mit `.text-white` oder ohne), `.text-grey`, `.text-red` | kein Hintergrund, `--red-fg`, 13 px; `--text-2`; `--red-fg`. `.text-white` allein hat keine Regel |
|
||||
| `.side-count`, `.side-logo` | kleine graue Zahl; Initiale ohne Verlauf |
|
||||
| `.card`, `.card-header`, `.card-body` | neutral: ohne Kasten, Kopf mit Haarlinie |
|
||||
| `div.row`, `.col`, `.col-6` (unter 768 px 100 %) | Raster ohne Look; `div.row`, damit `tr.row` unberührt bleibt |
|
||||
| `.dropdown`, `.dropdown-content`, `.dropdown-item` | weißes Popover mit `--shadow-pop` |
|
||||
| `.spinner` | grauer Ring |
|
||||
| `.page .page`, `.page .main-content` | Abstand und Breite aufgehoben |
|
||||
| `.fin-link`, `.legal-files a`, `.credit-cols a`, `.area-rows a` | feiner Unterstrich (`--line-strong`, beim Überfahren `currentColor`) |
|
||||
| `.ticket-tabs`, `.ticket-tab`, `.ticket-tab-active`, `.ticket-tab-content` | wie `.tabs` und `.tab` (Unterstrich statt Kasten); nicht in der Spec-Tabelle, aber `Tabs.jsx` bleibt bis Task 6 |
|
||||
| `.side*` („Altes Menü“) | siehe Abweichung oben; Task 5 löscht den Teil |
|
||||
|
||||
Das `:has()` der Alt-Dialoge ist nötig, damit `^\.overlay *\{` genau einmal trifft (Grundregel des neuen `.overlay > .dialog`).
|
||||
|
||||
Die Einblend-Animation des neuen `.overlay` (`fade`, Deckkraft 0 → 1) ließ `submitReachable` mit „kein offener Dialog gefunden“ scheitern, wenn der Dialog im ersten Bild geprüft wurde (`checkVisibility` mit `checkOpacity`). Alt-Dialoge hatten nie eine Animation, deshalb `animation: none` in KOMPAT. Task 7 muss dasselbe für `Dialog` lösen, z. B. mit `@media (prefers-reduced-motion: reduce) { .overlay, .dialog { animation: none; } }` (der Lauf setzt `reducedMotion: 'reduce'`).
|
||||
|
||||
Zusätzlich: `.datetime-pair input[type="date"]` bekommt `flex: 1.3`, weil das Datum im 520-px-Dialog sonst das Jahr abschneidet.
|
||||
|
||||
## Benutzt, aber ohne Regel (`undefinedUsed`)
|
||||
|
||||
Der Lauf nach diesem Task nennt 67 Namen. Der Lauf auf dem Stand vor P1 (`bd84d22`) nannte 58 davon schon; neu sind 9, alle gewollt:
|
||||
|
||||
| Name | Grund |
|
||||
|---|---|
|
||||
| `dot`, `empty-icon` | Task 3 hat die Regeln mit dem neuen `.badge` und `.empty` ersetzt; Task 6 baut `Badge` und `EmptyState` um (Prop `dot` und `icon` entfallen) |
|
||||
| `footer` | Regel nach Plan gelöscht; Task 5 löscht `Footer.jsx` |
|
||||
| `kpi`, `kpi-accent`, `kpi-label`, `kpi-sub` | Regeln nach Plan gelöscht; Task 6 lässt `Kpi` das neue `Stat` rendern |
|
||||
| `text-white` | Spec: allein keine Regel |
|
||||
| `ui-btn-ghost` | gleicher Look wie `.ui-btn`, braucht keine eigene Regel |
|
||||
|
||||
Die übrigen 58 hatten schon vor P1 keine Regel:
|
||||
- die Bootstrap-Namen aus `WorksheetList.jsx` (wirkungslos): `alert`, `alert-info`, `align-items-center`, `border-0`, `col-md-6`, `d-flex`, `fs-5`, `fs-6`, `justify-content-between`, `justify-content-md-end`, `mb-3`, `mb-4`, `me-1`, `me-2`, `ms-2`, `ms-3`, `mx-2`, `overflow-hidden`, `p-3`, `px-3`, `py-2`, `py-3`, `rounded-3`, `shadow-sm`, `spinner-border`, `text-dark`, `text-md-end`, `text-muted`, `visually-hidden`, `timeline`, `timeline-item`;
|
||||
- die Namen aus dem Plan: `p-4`, `btn-sm`, `status-filter`, `supporters-editor` (und `timeline` oben);
|
||||
- reine Haken ohne Styling: `credits-view`, `fin-abo-amount`, `fin-abo-main`, `fin-abo-next`, `fin-abo-product`, `fin-assign`, `fin-days`, `fin-inv-abo`, `fin-memo`, `fin-tax-card`, `fin-tax-note`, `fin-todo-main`, `fin-total-value`, `fin-tx-out`, `fin-wrap`, `legal-doc`, `offer-form`, `sp-ampel`, `sp-apps`, `sp-cleanup-body`, `status-filter-actions`, `worksheet-list`, `worksheet-supporters`.
|
||||
|
||||
## Bewusst offen (P1 Task 9)
|
||||
|
||||
Stand nach Task 9: `node "$W/farben.mjs" "$WT"` meldet **0 Treffer** in allen vier Gründen (`kontrast<3`, `dunkel-auf-akzent`, `weiss`, `verlauf`). Vorher waren es 41, 1, 18 und 11 (zusammen 71). Was danach noch an Farbliteralen im Quelltext steht, ist hier mit Zielpaket und gemessenem Kontrast festgehalten; keine Zeile liegt unter 3:1.
|
||||
|
||||
| Stelle | Was | Kontrast (gemessen) | Zielpaket |
|
||||
|---|---|---|---|
|
||||
| `global.css:269` (`.select`) | Pfeil als zwei `linear-gradient` mit hartem Stopp (`transparent 50%`), Wert aus dem Prototyp; `farben.mjs` führt ihn als „ausgenommen“ (`--ohne-ausnahmen` zeigt ihn) | `--text-2` auf Weiß 5,76:1 | bleibt (Spec) |
|
||||
| `global.css:1057` (KOMPAT, `select.form-control`) | derselbe Pfeil für das Altfeld | 5,76:1 | P10 (KOMPAT entfällt, `FormField` und die Altfelder gehen in P7 auf `.select`) |
|
||||
| `global.css:669` `.legal-version-current` (Tönung `rgba(16,185,129,.08)`), `:680` `.legal-tone-red td` (`rgba(239,68,68,.10)`), `:682` `.legal-tone-orange td` (`rgba(245,158,11,.10)`) | Alpha-Tönungen als Zeilenhintergrund | Text `--text` auf den Tönungen 14,3 bis 15,1:1; `.legal-deadline` rot (`--danger`) 5,43:1, orange (`--orange-fg`) 6,43:1 | P8 (Rechtliches, Tönungen werden Badge-Töne) |
|
||||
| `global.css:964` `.fin-confirm` (Tönung `rgba(245,158,11,.1)`) | Alpha-Tönung als Kastenhintergrund | Text `--text` auf der Tönung 15,1:1 | P7 (Finanzen) |
|
||||
| `src/components/serverpuls/chartColors.js` | Canvas-Farben, die einzige erlaubte Ausnahme im Quelltext (Spec) | Linie 18,88:1, Achse 5,76:1, Spitze 3,10:1; Raster und Teilstriche sind Hilfslinien ohne Text | bleibt (Spec) |
|
||||
| `src/components/Logo.jsx` (`fill="#111"`, `fill="#fff"`) | SVG-Marke mit zwei festen Farben | Weiß auf Schwarz 18,88:1 | bleibt (Marke) |
|
||||
|
||||
Außerhalb des Scans (er prüft nur CSS und JSX) und **nicht** unter „bewusst offen“, weil der Kontrast unter 3:1 liegt:
|
||||
|
||||
- `src/lib/previewEnv.js`, `DOT_COLORS` und `dotColor()`: Status-Punkte mit den Altfarben `#10b981` (2,54:1), `#f59e0b` (2,15:1), `#ef4444` (3,76:1) und `#a0aec0` (2,26:1) auf Weiß. Die Punkte sind höchstens 10 × 10 px groß und stehen immer neben dem Statustext (`ServicesPage`, `PreviewBlock`); der Browser-Check `dark-surface` ignoriert seit diesem Task Elemente bis 12 × 12 px. `tests/unit/previewEnv.test.mjs` hält die Hex-Werte fest, deshalb bleibt die Datei hier unverändert. Zielpaket: P5 (Projekte und Services): `dotColor()` liefert `var(--green-fg)`, `var(--orange-fg)`, `var(--red-fg)`, `var(--text-3)`, dazu der Test.
|
||||
|
||||
Nicht prüfbar im Werkzeug: Der Eingabebaustein des Chats kommt aus dem Kundenportal und ist dort eine leere Datei (`loadChatComposer`). Die Variablen `--chat-*` und die Meldungsfarben (`.chat-composer-note`, `.chat-file-size`, `.chat-audio-time`) sind laut Vorgabe gesetzt; ihr Laufzeitbild muss nach dem Livegang einmal im Chat angesehen werden (Zielpaket P6).
|
||||
@@ -0,0 +1,679 @@
|
||||
# WOMS „Weißes Design“ P10: Aufräumen – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P10 gibt es keine Übergangsschicht mehr. `react-icons` und `@tabler/icons-react` sind deinstalliert, die KOMPAT-Regeln, die alten Token-Aliase und alle toten Klassen sind gelöscht, `previewEnv.dotColor` ist entfernt. Sichtbar ändert sich nichts, das Bundle wird kleiner. Die Umleitung `/tickets?woid=N` auf `/tickets/N` bleibt.
|
||||
|
||||
**Architecture:**
|
||||
- Es wird nur gelöscht, umbenannt oder auf vorhandene Bausteine umgestellt. Die Bilder von `design-p9` sind die Referenz, ein Pixelvergleich (`vergleich.mjs`, gestartet über `run-browser.sh`) beweist nach jedem Löschschritt, dass nichts kippt.
|
||||
- Reihenfolge nach Risiko: Pakete, `dotColor`, Token-Aliase (reine Umbenennung), KOMPAT, tote Klassen, Inline-Styles und Regelprüfung, Gate und Tag.
|
||||
- Der Wächtertest `tests/unit/hygiene.test.mjs` hält die Ziele fest (drei Tests, je einer in Task 2, 4 und 5).
|
||||
- Die Ergebnisdatei `docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md` sammelt die Belege.
|
||||
|
||||
**Tech Stack:** wie P1. Node 24 (`readdirSync` mit `recursive`), npm 11, ripgrep 13 ohne PCRE2 (keine PCRE2-Option, keine Lookarounds).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Design-Tokens“ (Alias-Tabelle), „Übergangsschicht KOMPAT“, „Ticket als eigene Seite / Route und Links“, „Was gleich bleibt“, „Abnahme“ und Zeile P10. Dateidetails im Anhang `…-umbaukarte.md`, Abschnitt 5 (P10), 2.2, 2.3, 4 (D) und 7. Globale Regeln, Befehlsregeln (`env.sh`, `check.sh`, `run-browser.sh`) und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree `/home/webklar/apps/ticket.webklar.com/dev-design`, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR` (Scratchpad). Tests und Build laufen nur über `"$W/check.sh"`, Browser-Skripte nur über `"$W/shots.sh"` oder `"$W/run-browser.sh"`. Suche nur mit `rg` oder `git grep`, nie mit dem Shell-`grep` auf Dateien.
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P10 kommt hinzu:
|
||||
|
||||
- Voraussetzung sind die Tags `design-p0` bis `design-p9`, alle Diff-Prüfungen vergleichen gegen `design-p9`. Erlaubt ist nur Löschen, Umbenennen und Umstellen auf vorhandene Bausteine, keine neuen Funktionen, Klassen, Texte oder Tokens. Ausnahmen: Wächtertest, Ergebnisdatei, das Werkzeug außerhalb des Repos und, nur falls sie fehlt, die Deklaration `font-variant-numeric: tabular-nums` an `.num`, `.stat .v` und `.fin-amount` (Task 7 Step 4). Neue Ansichten braucht P10 nicht, das Gate läuft über alle bestehenden.
|
||||
- Nicht angefasst werden zusätzlich: die `?woid=`-Umleitung (`woidRedirectTarget` in `lib/ticketListUrl.js`, `<Navigate replace>` in `TicketsPage.jsx`), der Token-Block der Spec und die Klassen-Hooks aus „Was gleich bleibt“ (`data-testid`, `data-label`, `data-storage-done`, `data-hidden-hits`).
|
||||
- Klassensuche token-genau mit `(^|[^A-Za-z0-9_-])NAME([^A-Za-z0-9_-]|$)`. `rg -w` reicht nicht, weil `-` als Wortgrenze gilt und `card` sonst `fin-card` trifft.
|
||||
- `git add` mit ausdrücklichen Pfaden. Wo ein Schritt viele Dateien ändert, läuft vorher `git diff --name-only`, und `git add` bekommt genau diese geprüfte Liste.
|
||||
- **Entscheidung:** Pixelinvariante. Das Aussehen der Bilder von `design-p9` gilt, jede Abweichung ist ein Fehler und wird behoben. Ausgenommen sind nur Ansichten mit Rauschen (relative Zeiten wie „vor 5 Minuten“), das ein zweiter Lauf von `design-p9` selbst zeigt.
|
||||
- **Entscheidung:** Stoppregel für Icons. Liefert `rg "react-icons|@tabler" src` einen Treffer, stammt er aus einem früheren Paket oder aus Commits, die der Rebase von `origin/test` mitbrachte. P10 behebt ihn nach der Ersetzungsregel der Spec (Icon-Knopf wird Textknopf oder Textlink, Zeichen „ד „←“ „→“) in einem eigenen Commit vor Task 2 und nennt ihn in der Ergebnisdatei.
|
||||
- **Entscheidung:** Die Tokens des Spec-Blocks bleiben, auch ein ungenutztes. Nur die Aliase der Spec-Tabelle fallen, `--sp-1…6` bleiben. `Button` behält die Prop `size` in der Signatur (Spec „Knöpfe“), Aufrufe mit `size="sm"` verschwinden, weil sie nichts bewirken.
|
||||
- **Entscheidung:** Farbliterale in JSX sind nur in `src/components/Logo.jsx` (Markenzeichen, `#111` und `#fff` wie `public/favicon.svg`) und `src/components/serverpuls/chartColors.js` erlaubt. Inline-`<svg>` bleibt nur in `Logo.jsx` und `Sparkline.jsx`. Der Check `icon-svg` nimmt `svg.sp-spark` (P9) und `.woms-chat .chat-composer *` (P0) in `checks.mjs` aus, dazu das Gate alles unter `.woms-chat` (`ignore`, P6). P10 fügt keine Ausnahme hinzu.
|
||||
- **Entscheidung:** Beim Umstellen gelten die Regeln „Bausteine benutzen“ der Übersicht und die CSS-Reihenfolge: In jedem `<Dialog as="form">` trägt jeder Knopf außer dem Absenden-Knopf `type="button"`, ein `Card` ohne Titel und Aktionen rendert kein `h2`, und Bereichs-CSS (`chat.css`, `serverpuls.css`, `pages/AdminPage.css`) lädt vor `global.css`. Überstimmt eine globale Regel es nach dem Löschen nicht mehr, braucht der Bereichs-Selektor eine höhere Spezifität, nie `!important`.
|
||||
- **Entscheidung:** Die Ergebnisdatei hat sieben Abschnitte: 1 Token-Aliase, 2 KOMPAT-Inventur, 3 Tote Klassen, 4 Inline-Styles, 5 Regelprüfung und Invarianten, 6 Bundle, 7 Offene Punkte. Task 4 legt sie mit allen Überschriften an, jeder Task nimmt seinen Abschnitt in seinen Commit auf.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Aus der Übersicht gehört P10 kein Punkt allein. Alle fünf laufen aber als Ansichten im Gate `p10` mit (`ticket-altdaten*`, `*-lang*`, `fehler-*`, `tasten-*`, `ticket-viel`, `tickets-suche`) und müssen unverändert bleiben. P10 verantwortet sechs eigene Prüfungen:
|
||||
|
||||
1. **Nichts Sichtbares ändert sich.** Pixelvergleich `design-p9` gegen P10 über alle Ansichten in allen Viewports (Standard 1440×900 und 390×844, bei `dlg-*` auch 1440×700 und 390×700), keine abweichende Ansicht außerhalb des Rauschens (Task 4, 5, 6 und 8).
|
||||
2. **Keine noch gebrauchte Regel geht verloren.** Jede KOMPAT-Klasse hat eine Inventurzeile mit Nutzerzahl, jeder Restnutzer wird vor dem Löschen umgestellt (Task 5). Kein `var(--…)` zeigt ins Leere, `undefinedUsed` ist leer (Task 4).
|
||||
3. **Die Icon-Pakete fehlen nirgends.** `rg` meldet 0 Treffer, der Build ist grün, kein `console-error` in irgendeiner Ansicht (ein fehlender Import wäre zur Laufzeit ein `pageerror`), das Lock passt zu `package.json` (Task 2).
|
||||
4. **Die `?woid=`-Umleitung bleibt.** Ansicht `alt-link` im Gate, Test `woidRedirectTarget` aus P2 grün, `TicketsPage.jsx` und `ticketListUrl.js` unverändert (Task 8).
|
||||
5. **Die Spec-Invarianten halten:** alle `data-testid`, die Zahl der `window.confirm`, Hooks und Lib-Logik nur wie in den Paketplänen geändert (Task 7 Step 5, Task 8 Review).
|
||||
6. **Die übergreifenden Gestaltungsregeln gelten:** höchstens ein `.btn-primary` je `.page-head` (Spec-Regel 4, Check `max-primary` im Gate, Task 1 Step 6 und Task 8) und `tabular-nums` an Zahlen und Beträgen (Spec-Regel 6, `rg` in Task 7 Step 4).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Ausgangslage, Referenzbilder, Werkzeug
|
||||
|
||||
**Files** (alle unter `$W/`, kein Commit im Repo):
|
||||
- Create: `vergleich.mjs`, `token-check.mjs`
|
||||
- Modify: `css-klassen.mjs` (Flag `--ohne-ausnahmen`), `checks.mjs` (Check `max-primary`), `gates.mjs` (Gate `p10`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tag `design-p9`, `build.sh`, `shots.sh`, `run-browser.sh`, `check.sh`, `css-klassen.mjs`, `farben.mjs`, `GATES.p9` (P0 bis P9).
|
||||
- Produces:
|
||||
- `"$W/run-browser.sh" "$W/vergleich.mjs" <A> <B> [--rauschen <A2>] [--out <Ordner>]` → JSON `{ geprueft, anders: [{ view, viewport, diffPixels, bbox }], rauschen: [view], fehlt: [view] }`. Exit 1, wenn `anders` oder `fehlt` nicht leer ist. A, B und A2 sind Ausgabeordner von `shots.sh` (`<w>x<h>/<id>.png`, z. B. `1440x900/tickets.png` und `390x844/tickets.png`).
|
||||
- `node "$W/token-check.mjs" <worktree> [--fix]` → JSON `{ legacy: [{ name, ersatz, file, line, art }], undefinedUsed: [{ name, file, line }], mitFallback: [{ name, file, line }], definedUnused: [name] }`. Exit 1, wenn `legacy` oder `undefinedUsed` nicht leer ist.
|
||||
- `css-klassen.mjs --ohne-ausnahmen`: ignoriert die Ausnahmeliste und gibt zusätzlich `nurAusnahme: [name]` aus, die Klassen, die nur durch die Liste als benutzt gelten.
|
||||
- Check `max-primary` in `checks.mjs`: Verstoß `{ check: 'max-primary', selector: '.page-head', detail }` für jedes sichtbare `.page-head` mit mehr als einem sichtbaren `.btn-primary`.
|
||||
- `GATES.p10` nach dem Schema der Übersicht (Step 6).
|
||||
|
||||
- [ ] **Step 1: Paketstart und Stand prüfen**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
git fetch origin && git merge-base --is-ancestor design-p9 HEAD && git rebase origin/test && "$W/check.sh"
|
||||
git log --oneline origin/test..feature/terminplaner-tp2 -- src package.json package-lock.json tests
|
||||
git tag --list 'design-p*'; git log --oneline design-p9..origin/test; git status --short
|
||||
```
|
||||
Expected:
|
||||
- Erste Zeile: `design-p9` ist Vorfahre (sonst Exit 1: stoppen und melden, der Rebase schrieb die Pakete um), der Rebase läuft ohne Konflikt, `check.sh` endet mit Exit 0, `# fail 0`, `# pass` ist `N9` (notieren).
|
||||
- Zweite Zeile (Abgleich mit `feature/terminplaner-tp2`, P10 berührt fast alle Dateien unter `src`): leer. Gibt sie etwas aus, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
|
||||
- Tags `design-p0` bis `design-p9`, `git log` und `git status` leer. Kommen fremde Commits mit, werden ihre Dateien in den folgenden Tasks wie alle anderen geprüft.
|
||||
|
||||
- [ ] **Step 2: Voraussetzungen im Werkzeug**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
rg -n "GATES\.p9|p1:" "$W/gates.mjs"; rg -n "ignore" "$W/gates.mjs"; rg -n "sp-spark" "$W/checks.mjs"
|
||||
ls "$W/check.sh" "$W/run-browser.sh" "$W/css-klassen.mjs" "$W/farben.mjs" "$W/build.sh" "$W/shots.sh"
|
||||
```
|
||||
Expected: Gates `p1` und `p9`, im `ignore` nur `.woms-chat` (P6, `.ticket-projects` strich P5), Ausnahme `svg.sp-spark` und alle sechs Dateien sind da. Fehlt etwas, wird gestoppt und gemeldet.
|
||||
|
||||
- [ ] **Step 3: Ausgangswerte des Bundles festhalten**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
git log design-p0 --format=%B | rg "Ausgangswerte" &&
|
||||
"$W/check.sh" --nur-build &&
|
||||
{ find dist -type f \( -name '*.js' -o -name '*.css' -o -name '*.ttf' \) -printf '%s %p\n'; for f in dist/assets/*.js dist/assets/*.css; do echo "$f gzip=$(gzip -9 -c "$f" | wc -c)"; done; wc -l src/styles/*.css src/pages/*.css; } | tee "$W/p10-baseline-p9.txt"
|
||||
```
|
||||
Expected: die P0-Zeile `Ausgangswerte: <Testzahl>, JS <x> B, CSS <y> B` (Gegenprobe der Umbaukarte: JS 680.818 B, CSS 64.433 B, Lato 96.184 B, 2.079 CSS-Zeilen), Build grün, keine `.ttf` in `dist`, alle Zahlen in `$W/p10-baseline-p9.txt`.
|
||||
|
||||
- [ ] **Step 4: Referenzbilder und Rauschmaß**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9-ref && "$W/shots.sh" p9-ref "$W/shots/p9-ref" --gate p9 && "$W/shots.sh" p9-ref "$W/shots/p9-ref2" --gate p9 && ls "$W/shots/p9-ref/1440x900" | wc -l`
|
||||
Expected: beide Läufe Exit 0, je Ansicht ein Bild je Viewport (`1440x900` und `390x844`, bei `dlg-*` zusätzlich `1440x700` und `390x700`). Die Zahl der Ansichten ist die Sollzahl aller späteren Läufe (notieren).
|
||||
|
||||
- [ ] **Step 5: `vergleich.mjs` schreiben und prüfen**
|
||||
|
||||
Umsetzung:
|
||||
- Das Skript startet Chromium selbst (Ladeweise wie `shots.mjs`) und läuft deshalb nur über `run-browser.sh`.
|
||||
- Die PNGs werden in Playwright-Chromium (`about:blank`) per `createImageBitmap` und Canvas dekodiert und pixelweise verglichen. Eine andere Bildgröße ist `anders` und nennt beide Größen. `bbox` ist der kleinste Rahmen um alle abweichenden Pixel.
|
||||
- `--out` bekommt je abweichender Ansicht ein Bild unter `<w>x<h>/<id>.png`, abweichende Pixel rot.
|
||||
- Eine Ansicht mit Abweichung zwischen A und A2 zählt als `rauschen`, außer die Abweichung zwischen A und B reicht über deren `bbox` hinaus. Dann ist sie `anders`.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
cp -r "$W/shots/p9-ref" "$TMPDIR/probe" && cp "$TMPDIR/probe/1440x900/tickets.png" "$TMPDIR/probe/1440x900/kunden.png"
|
||||
"$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$TMPDIR/probe"; echo "exit $?"
|
||||
"$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p9-ref2"; echo "exit $?"
|
||||
```
|
||||
Expected: Der erste Aufruf nennt genau `kunden` bei 1440×900 als `anders`, Exit 1. Der zweite endet mit Exit 0 oder nennt wenige Rausch-Ansichten. Jede wird einmal angesehen und in `$W/p10-rauschen.txt` notiert.
|
||||
|
||||
- [ ] **Step 6: `token-check.mjs`, Flag, Check `max-primary` und Gate**
|
||||
|
||||
Umsetzung von `token-check.mjs`:
|
||||
- Es liest `src/**/*.{css,jsx,js}` und schreibt nur dort. Definiert ist ein Name durch `--name:` in CSS oder den Schlüssel `'--name'` in JSX-Objekten, benutzt durch `var(--name`.
|
||||
- `legacy` enthält jede Nutzung und jede Definition eines Alias-Namens der Tabelle in Task 4 (`art` ist `nutzung` oder `definition`, `ersatz` der neue Name oder der Wert `4px` bzw. `none`).
|
||||
- `undefinedUsed` sind benutzte, nirgends definierte Namen ohne Rückfallwert, mit Rückfallwert landen sie in `mitFallback`.
|
||||
- `--fix` schreibt nur die `nutzung`-Einträge um (`var(--alt)` und `var(--alt, …)`), `--sidebar-w-collapsed` nie.
|
||||
|
||||
Dazu kommen:
|
||||
- das Flag in `css-klassen.mjs`,
|
||||
- der Check `max-primary` in `checks.mjs` (Spec, Gestaltungsregel 4: höchstens ein `.btn-primary` je Seitenkopf), mit `detail` `<n> .btn-primary`,
|
||||
- `GATES.p10` in `gates.mjs`. P10 hat keine eigenen Ansichten, `icon-svg` gilt deshalb für alle (`'*'` ist der Präfix-Platzhalter für alle Ansichten, wie in P2), mit dem `ignore` aus `p9` unverändert. `step-failed`, `external-request` und `console-error` sind wie in jedem Gate aktiv.
|
||||
|
||||
```js
|
||||
const iconSvg = GATES.p9.scoped['icon-svg']
|
||||
GATES.p10 = {
|
||||
...GATES.p9,
|
||||
checks: [...GATES.p9.checks, 'max-primary'],
|
||||
scoped: { ...GATES.p9.scoped, 'icon-svg': { ...iconSvg, views: ['*'] } },
|
||||
}
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
mkdir -p "$TMPDIR/tc/src" && printf 'a{color:var(--nope);border:1px solid var(--border)}' > "$TMPDIR/tc/src/x.css"
|
||||
node "$W/token-check.mjs" "$TMPDIR/tc"; echo "exit $?"
|
||||
node "$W/token-check.mjs" . | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.legacy.length, j.undefinedUsed.length)"
|
||||
node "$W/css-klassen.mjs" . --ohne-ausnahmen | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.length, j.nurAusnahme.length)"
|
||||
mkdir -p "$TMPDIR/probe-primary" && printf '<!doctype html><meta charset="utf-8"><title>Probe</title><div class="page-head"><h1>Test</h1><button class="btn btn-primary">A</button><button class="btn btn-primary">B</button></div>' > "$TMPDIR/probe-primary/index.html"
|
||||
"$W/shots.sh" probe "$TMPDIR/probe-primary-out" --static "$TMPDIR/probe-primary" --only tickets --gate p10; echo "exit $?"
|
||||
rg -o "max-primary" "$TMPDIR/probe-primary-out/report.json" | wc -l
|
||||
```
|
||||
Expected:
|
||||
- Der erste Aufruf endet mit Exit 1, `undefinedUsed` nennt `--nope`, `legacy` nennt `--border` mit `ersatz` `--line`.
|
||||
- Der zweite zeigt eine Zahl über 0 für `legacy` (die Alias-Definitionen stehen noch) und 0 für `undefinedUsed`.
|
||||
- Der dritte nennt zwei Zahlen, sie werden notiert.
|
||||
- Die Probe mit zwei Primärknöpfen in einem `.page-head` endet mit Exit 1, die letzte Zeile nennt eine Zahl über 0 (der Check `max-primary` schlägt an).
|
||||
|
||||
- [ ] **Step 7: Gate `p10` gegen `design-p9`**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/shots.sh" p9-ref "$W/shots/p9-ref-p10" --gate p10; echo "exit $?"`
|
||||
Expected: Exit 0 (die Bilder dieses Laufs werden nicht gebraucht). Ein Treffer `icon-svg` folgt der Stoppregel für Icons. Ein Treffer `max-primary` ist ein Fehler eines früheren Pakets: Der Agent stoppt und meldet ihn dem Eigentümer, weil der überzählige Knopf das Aussehen ändern würde.
|
||||
|
||||
### Task 2: Icon-Pakete entfernen
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/unit/hygiene.test.mjs`
|
||||
- Modify: `package.json` (Zeilen 6 und 11), `package-lock.json`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nichts. Voraussetzung ist `rg "react-icons|@tabler" src` = 0.
|
||||
- Produces: ein Lock ohne `@tabler/icons-react`, `@tabler/icons` und `react-icons`, dazu `hygiene.test.mjs` mit den Helfern `files`, `read`, `offenders(re, only)`, auf denen Task 4 und 5 aufbauen.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
```js
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const files = readdirSync(path.join(root, 'src'), { recursive: true })
|
||||
.filter((f) => /\.(jsx?|css)$/.test(f))
|
||||
.map((f) => path.join('src', f))
|
||||
const read = (rel) => readFileSync(path.join(root, rel), 'utf8')
|
||||
const offenders = (re, only = /./) => files.filter((f) => only.test(f) && re.test(read(f)))
|
||||
|
||||
test('Icon-Pakete sind weder Abhängigkeit noch Import', () => {
|
||||
const pkg = JSON.parse(read('package.json'))
|
||||
for (const n of ['react-icons', '@tabler/icons-react']) assert.equal(pkg.dependencies[n], undefined, n)
|
||||
assert.deepEqual(offenders(/react-icons|@tabler/), [])
|
||||
})
|
||||
```
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs`
|
||||
Expected: FAIL (`package.json` führt beide Pakete).
|
||||
|
||||
- [ ] **Step 2: Beweisen, dass nichts mehr importiert**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler" src`
|
||||
Expected: keine Ausgabe. Bei einem Treffer gilt die Stoppregel der Global Constraints.
|
||||
|
||||
- [ ] **Step 3: Deinstallieren**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 npm uninstall react-icons @tabler/icons-react --no-audit --no-fund`
|
||||
Expected: Exit 0.
|
||||
|
||||
- [ ] **Step 4: Lock und Index prüfen**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
git status --short; git diff package.json | rg "^[-+] "; rg -c "tabler|react-icons" package.json package-lock.json; git diff --numstat package-lock.json
|
||||
npm ls react-icons @tabler/icons-react 2>&1 | rg "empty"; npm ci --dry-run --no-audit --no-fund; echo "exit $?"
|
||||
```
|
||||
Expected:
|
||||
- `git status` zeigt genau `M package-lock.json`, `M package.json` und `?? tests/unit/hygiene.test.mjs`, keine `D`-Zeile (`node_modules` ist nicht getrackt).
|
||||
- Der Diff von `package.json` hat genau zwei `-`-Zeilen, `rg -c` gibt nichts aus.
|
||||
- `numstat` zeigt 0 hinzugefügte Zeilen. Stehen dort wenige, sind es höchstens geänderte Flags (`"peer"`, `"dev"`), und jede `+`-Zeile wird angesehen.
|
||||
- `npm ls` meldet `(empty)`, `npm ci --dry-run` endet mit Exit 0.
|
||||
|
||||
- [ ] **Step 5: Tests und Build**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh"`
|
||||
Expected: Exit 0, `# fail 0`, `# pass` = `N9 + 1`, Build grün.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add package.json package-lock.json tests/unit/hygiene.test.mjs
|
||||
git commit -m "Aufraeumen: react-icons und @tabler/icons-react deinstalliert (src ohne Icon-Importe), Waechtertest" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: `dotColor` und Icon-Kommentare
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/previewEnv.js` (Zeile 1 und Zeilen 15–18 laut Ausgangsstand), `tests/unit/previewEnv.test.mjs` (Zeile 5 und der Test `dotColor`, Zeilen 21–29)
|
||||
- Modify: `src/lib/navLinks.js` (Zeile 1), `tests/unit/navLinks.test.mjs` (Zeile 5)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `isValidEnvName`, `buildEnvPayload`, `isDevPreview`, `envValueTooLong`, `MAX_ENV_VARS` aus `previewEnv.js`, unverändert.
|
||||
- Produces: `previewEnv.js` ohne `dotColor` und `DOT_COLORS`.
|
||||
|
||||
- [ ] **Step 1: Beweisen, dass `dotColor` unbenutzt ist**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "dotColor|DOT_COLORS" src tests`
|
||||
Expected: Treffer nur in `src/lib/previewEnv.js` und `tests/unit/previewEnv.test.mjs` (P5 hat `PreviewBlock` und `ServicesPage` umgestellt). Weitere Treffer sind ein Fehler eines früheren Pakets.
|
||||
|
||||
- [ ] **Step 2: Test zuerst anpassen**
|
||||
|
||||
Der Test `dotColor` und der Name im `loadModule`-Destrukturieren entfallen.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/previewEnv.test.mjs`
|
||||
Expected: PASS mit 4 Tests (`isValidEnvName`, `buildEnvPayload`, `isDevPreview`, `envValueTooLong`).
|
||||
|
||||
- [ ] **Step 3: Funktion löschen**
|
||||
|
||||
`DOT_COLORS` und `dotColor` fallen, der Kopfkommentar heißt „(Variablen, Dev-Erkennung)“.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/previewEnv.test.mjs; rg -n "dotColor|DOT_COLORS|#10b981|#f59e0b|#a0aec0" src tests`
|
||||
Expected: PASS, keine Treffer.
|
||||
|
||||
- [ ] **Step 4: Icon-Kommentare**
|
||||
|
||||
- `src/lib/navLinks.js` Zeile 1 wird `// Eintraege der Sidebar als reine Daten (unter Node testbar); Navbar.jsx rendert sie als Textliste.` Der zweite Kommentarsatz zu Unterpunkten und `count` bleibt.
|
||||
- `tests/unit/navLinks.test.mjs` Zeile 5 wird `// Sidebar: Eintraege als reine Daten (Navbar.jsx rendert sie als Textliste)`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n -i "(^|[^a-z])icons?([^a-z]|$)" src tests --glob '!*.css' --glob '!hygiene.test.mjs'`
|
||||
Expected: keine Treffer, höchstens Kommentare zum fremden Chat-Baustein in `src/components/chat` und `src/lib/loadChatComposer.js`. Jeder andere Treffer wird umformuliert. „favicon“ und der Wächtertest zählen nicht.
|
||||
|
||||
- [ ] **Step 5: Test**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-tests`
|
||||
Expected: Exit 0, `# fail 0`, `# pass` = `N9` (hygiene +1, `dotColor` −1).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/previewEnv.js tests/unit/previewEnv.test.mjs src/lib/navLinks.js tests/unit/navLinks.test.mjs
|
||||
git commit -m "Aufraeumen: previewEnv.dotColor samt Test entfernt, Icon-Kommentare in navLinks angepasst" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 4: Token-Aliase abbauen
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css` (Alias-Block in `:root`), alle Dateien unter `src/` mit alten Namen (Liste aus Step 3)
|
||||
- Create: `docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md` (Überschriften 1 bis 7, Abschnitt 1 gefüllt)
|
||||
- Test: `tests/unit/hygiene.test.mjs` (zweiter Test)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `$W/token-check.mjs` (Task 1) und die Alias-Tabelle der Spec:
|
||||
|
||||
| Alter Name | Neu |
|
||||
|---|---|
|
||||
| `--surface-1`, `--surface-3`, `--surface`, `--dark-bg`, `--dark-card-bg` | `--bg` |
|
||||
| `--surface-2`, `--surface-hover` | `--bg-hover` |
|
||||
| `--border`, `--gray-700`, `--admin-line` | `--line` |
|
||||
| `--border-strong`, `--admin-line-strong` | `--line-strong` |
|
||||
| `--border-accent` | `--accent-ring` |
|
||||
| `--text-muted`, `--text-faint`, `--gray-800` | `--text-2` |
|
||||
| `--dark-text`, `--gray-900` | `--text` |
|
||||
| `--accent-2`, `--green-secondary` | `--accent-hover` |
|
||||
| `--green-primary` | `--accent` |
|
||||
| `--info`, `--warn`, `--ok` | `--blue-fg`, `--orange-fg`, `--green-fg` |
|
||||
| `--r-sm`, `--r-md`, `--r-lg`, `--r-pill` | `--radius`, `--radius-lg`, `--radius-lg`, Wert `4px` |
|
||||
| `--shadow-1`, `--shadow-2`, `--shadow-3` | `none`, `--shadow-btn`, `--shadow-pop` |
|
||||
| `--sidebar-w-collapsed` | entfällt, kein Nutzer erlaubt |
|
||||
|
||||
- Produces: `:root` ohne Aliase, `token-check` meldet `legacy` und `undefinedUsed` leer.
|
||||
|
||||
- [ ] **Step 1: Failing Test ergänzen**
|
||||
|
||||
```js
|
||||
const LEGACY = ['surface-1', 'surface-2', 'surface-3', 'surface-hover', 'surface', 'dark-bg', 'dark-card-bg', 'border', 'border-strong', 'border-accent',
|
||||
'gray-700', 'gray-800', 'gray-900', 'admin-line', 'admin-line-strong', 'text-muted', 'text-faint', 'dark-text', 'accent-2', 'green-secondary',
|
||||
'green-primary', 'info', 'warn', 'ok', 'r-sm', 'r-md', 'r-lg', 'r-pill', 'shadow-1', 'shadow-2', 'shadow-3', 'sidebar-w-collapsed']
|
||||
test('keine alten Token-Namen mehr (Aliase sind gelöscht)', () => {
|
||||
const re = new RegExp(`(^|[^A-Za-z0-9-])--(${LEGACY.join('|')})([^A-Za-z0-9-]|$)`)
|
||||
assert.deepEqual(offenders(re), [])
|
||||
})
|
||||
```
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs`
|
||||
Expected: FAIL im neuen Test.
|
||||
|
||||
- [ ] **Step 2: Bestandsaufnahme**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
node "$W/token-check.mjs" . > "$TMPDIR/token-vorher.json"; node -e "const j=JSON.parse(require('fs').readFileSync(process.argv[1]));const c={};for(const e of j.legacy)c[e.name+' '+e.art]=(c[e.name+' '+e.art]||0)+1;console.log(c)" "$TMPDIR/token-vorher.json"
|
||||
```
|
||||
Expected: Zahl der Nutzungen und Definitionen je Alias. Sie kommt in Abschnitt 1 der Ergebnisdatei (Spalten: alter Name, Nutzungen vorher, ersetzt durch).
|
||||
|
||||
- [ ] **Step 3: Nutzungen umbenennen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/token-check.mjs" . --fix; git diff --name-only`
|
||||
Expected: Es ändern sich nur `.css`, `.jsx` und `.js` unter `src/`. Das ist eine reine Umbenennung, der Wert jedes Tokens bleibt gleich.
|
||||
|
||||
- [ ] **Step 4: Alias-Block löschen**
|
||||
|
||||
In `global.css` fallen der Alias-Block hinter dem Token-Block samt Kommentar und jede übrige Definition eines Alias-Namens, auch außerhalb von `:root`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs && node "$W/token-check.mjs" . | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.legacy.length, j.undefinedUsed.length, j.mitFallback.length)"`
|
||||
Expected: PASS und `0 0` plus eine Zahl für `mitFallback`. Jeder Eintrag dort wird angesehen: Der Rückfallwert fällt, wenn das Token existiert, sonst steht der Eintrag in Abschnitt 7.
|
||||
|
||||
- [ ] **Step 5: Bauen, Gate, Pixelvergleich**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10-t4" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10-t4" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-t4-diff"`
|
||||
Expected: Gate Exit 0, Vergleich Exit 0. Weicht eine Ansicht ab, hat `--fix` einen Namen falsch ersetzt oder eine Definition lag in einer Datei, die das Skript nicht liest. Das wird behoben.
|
||||
|
||||
- [ ] **Step 6: Ergebnisdatei und Commit**
|
||||
|
||||
Die Ergebnisdatei wird mit den Überschriften 1 bis 7 angelegt, Abschnitt 1 bekommt die Tabelle aus Step 2.
|
||||
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
git diff --name-only
|
||||
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md tests/unit/hygiene.test.mjs <die geprüften Dateien aus der Liste oben>
|
||||
git commit -m "Aufraeumen: Token-Aliase der Altnamen durch die neuen Tokens ersetzt und geloescht" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 5: KOMPAT-Block entfernen
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css` (Block `/* ===== KOMPAT (bis P10) ===== */` und der Gliederungskommentar am Dateianfang), Dateien mit Restnutzern (Liste aus Step 3)
|
||||
- Modify: Ergebnisdatei Abschnitt 2
|
||||
- Test: `tests/unit/hygiene.test.mjs` (dritter Test)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: die Bausteine aus P1 (`.btn`, `.btn-primary`, `.btn-quiet`, `.link`, `.link-danger`, `.badge`, `.field`, `.input`, `.select`, `.textarea`, `.msg`, `.muted`, `.dialog`, `.overlay`, `.seg button.on`) und `Dialog`, `Card`, `Badge`, `StatusPill`.
|
||||
- Produces: `global.css` ohne KOMPAT. Kein Selektor der Spec-Tabelle „Übergangsschicht KOMPAT“ ist übrig, `.page` bleibt als Wurzelklasse der Seiten.
|
||||
|
||||
- [ ] **Step 1: Failing Test ergänzen**
|
||||
|
||||
```js
|
||||
test('keine Übergangsschicht und kein !important im CSS', () => {
|
||||
assert.deepEqual(offenders(/KOMPAT|!important/, /\.css$/), [])
|
||||
})
|
||||
```
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/hygiene.test.mjs`
|
||||
Expected: FAIL im neuen Test.
|
||||
|
||||
- [ ] **Step 2: Grenzen und Inventur**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
read A B < <(awk '/KOMPAT \(bis P10\)/{a=NR; next} a && /^\/\* (=====|---)/{print a+1, NR-1; f=1; exit} END{if(a&&!f)print a+1, NR}' src/styles/global.css); echo "KOMPAT $A-$B"
|
||||
sed -n "${A},${B}p" src/styles/global.css | rg -o "\.[a-z][a-z0-9_-]*" | sort -u | sed 's/^\.//' | while read c; do echo "$(rg -l "(^|[^A-Za-z0-9_-])${c}([^A-Za-z0-9_-]|$)" src --glob '!*.css' | wc -l) $c"; done | sort -n | tee "$TMPDIR/kompat-inventur.txt"
|
||||
```
|
||||
Expected:
|
||||
- Der Block endet an der nächsten Kopfzeile `/* =====` oder `/* ---`, sonst am Dateiende.
|
||||
- Die Inventur hat 0 Nutzer für jede Klasse der Tabelle in Step 3. Bekannte Ausnahmen: `overlay` (nur `src/components/ui/Dialog.jsx`), `row` (nur an `<tr>`) und Klassen, die auch ein neuer Baustein trägt (`btn`, `badge`, `field`, `link`). Deren Regel wandert nach Step 4 in Block 3.
|
||||
- Die Liste kommt als Tabelle (Klasse, Nutzer, Entscheidung) in Abschnitt 2 der Ergebnisdatei.
|
||||
|
||||
- [ ] **Step 3: Restnutzer finden und umstellen**
|
||||
|
||||
| KOMPAT-Zeile der Spec | Gesuchte Namen | Ersatz |
|
||||
|---|---|---|
|
||||
| `.ui-card*` | `ui-card` | `Card` (ohne Titel und Aktionen rendert es kein `h2`) oder `<section>` mit `h2.section` |
|
||||
| `.ui-btn*` | `ui-btn`, `-primary`, `-ghost`, `-danger`, `-sm`, Muster `ui-btn-${…}` | `btn`, `btn btn-primary`, `link-danger`, Zeilenaktion `link` |
|
||||
| `.form-group`, `.form-label`, `.form-control` | dieselben drei | `field`, Label im `.field`, `input`, `select`, `textarea` |
|
||||
| `.badge-*`, `.status-*`, `.priority-*` | `badge-` und `status-` mit `muted`, `info`, `ok`, `warn`, `danger`, `open`, `working`, `awaiting`, `assigned`, `closed`; `priority-critical`; Muster `badge-${…}`, `status-${…}` | `Badge`, `StatusPill` |
|
||||
| `.overlay` (Altform), `.overlay-content`, `.overlay-close` | `overlay-content`, `overlay-close`, `overlay` außerhalb von `Dialog.jsx`, `fin-dialog` (760 px, laut P7 tot) | `Dialog` |
|
||||
| `.seg button.active` | kein festes Muster: jedes `seg` in JSX ansehen | Knopf mit `on` und `aria-pressed` |
|
||||
| `.bg-red`, `.text-white`, `.text-grey`, `.text-red` | dieselben vier | `msg err` bzw. `muted` |
|
||||
| `.side-count`, `.side-logo` | dieselben zwei | schon entfernt: `.side-logo` in P4, `.side-count` in P6. Ein Treffer ist ein Rest und wird entfernt |
|
||||
| `.btn-dark`, `.btn-teal` | dazu `btn-green` | `btn btn-primary` |
|
||||
| `.card*`, `.row`, `.col*`, `.dropdown*` | `card`, `card-header`, `card-body`, `col`, `col-6`, `dropdown*`, `row` außerhalb von `<tr>` | entfallen |
|
||||
| `.spinner` | `spinner` | Text „Wird geladen …“ |
|
||||
| `.page .page`, `.page .main-content` | `main-content` | entfällt (Step 5) |
|
||||
| Links in Bestandsklassen | `fin-link`, `credit-cols`, `area-rows`. Die Klasse `legal-files` bleibt (P8), nur die Regel `.legal-files a` fällt. | Klasse `link` am `<a>`, sonst die Regel in den Block des Nutzers |
|
||||
|
||||
In jedem `<Dialog as="form">`, das dabei entsteht oder angefasst wird, trägt jeder Knopf außer dem Absenden-Knopf `type="button"`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n '(^|[^A-Za-z0-9_-])(ui-(card|btn)|form-(group|label|control)|(badge|status)-(muted|info|ok|warn|danger|open|working|awaiting|assigned|closed)|priority-critical|overlay-(content|close)|fin-dialog|bg-red|text-(white|grey|red)|side-(count|logo)|btn-(dark|teal|green)|spinner|dropdown[a-z-]*|card|card-header|card-body|col|col-6|main-content|fin-link|credit-cols|area-rows)([^A-Za-z0-9_-]|$)' src --glob '!*.css'; rg -n '(badge|status|ui-btn)-\$\{' src; rg -n 'className="[^"]*(^|[^A-Za-z0-9_-])row([^A-Za-z0-9_-]|")' src | rg -v '<tr'`
|
||||
Expected: keine Treffer. Ein Treffer in einem Kommentar wird umformuliert, jeder Code-Treffer nach der Tabelle umgestellt, eine Gruppe je Commit (Meldung „Aufraeumen: Restnutzer von <Gruppe> auf <Baustein> umgestellt“).
|
||||
|
||||
- [ ] **Step 4: Regeln, die ein Baustein noch braucht**
|
||||
|
||||
Entscheidung: Das Aussehen der Bilder von `design-p9` gilt. Eine KOMPAT-Regel, deren Selektor auch einen neuen Baustein trifft, wird nicht gelöscht. Ihre Deklarationen gehören dann an den Baustein in Block 3, nur die alten Selektoren fallen. Bekannter Fall ist `.overlay`, die Hülle von `Dialog`:
|
||||
- Ist die KOMPAT-Zeile für die Altform ein eigener Selektor (`overlay-content`, `:has(…)`), fällt sie weg.
|
||||
- Hat sie die Basisregel `.overlay` selbst verändert (`overflow-y: auto`, Abstand 24/16 px), gelten die Werte, die die Bilder von `design-p9` für `Dialog` zeigen, und bleiben in der Basisregel.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "^\.(overlay|dialog)\b" src/styles/global.css`
|
||||
Expected: eine Basisregel `.overlay` mit der Mobilregel bei 860 px, ohne Verweis auf `overlay-content`.
|
||||
|
||||
- [ ] **Step 5: Block löschen, Verschachtelung statisch beweisen**
|
||||
|
||||
Der Block samt Marker fällt, der Gliederungskommentar nennt danach vier Blöcke statt fünf.
|
||||
|
||||
Entscheidung: `.page` bleibt als Wurzelklasse jeder Seite. Gelöscht werden nur die Verschachtelungsregel `.page .page, .page .main-content` und die alte `.main-content`-Regel.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
node --test tests/unit/hygiene.test.mjs
|
||||
rg -n "KOMPAT|main-content" src
|
||||
rg -n 'className=\{?["`]([^"`]* )?page([ "`}]|$)' src --glob '*.jsx' | rg -v '^src/pages/'
|
||||
rg -n '<[A-Za-z]+Page\b' src --glob '!App.jsx'
|
||||
```
|
||||
Expected: PASS und drei leere Ausgaben. Es gibt also kein `KOMPAT` und kein `main-content` mehr, die Klasse `page` steht nur in `src/pages/*.jsx` (der Treffer-Filter erfasst nur Klassenstellen, keine Props wie `page` in `Pager`), und nur `App.jsx` rendert `*Page`-Komponenten. Eine Seite in einer Seite ist damit ausgeschlossen. Ein Treffer, der nur ein Wort im Kommentar ist, wird umformuliert.
|
||||
|
||||
- [ ] **Step 6: Bauen, Gate, Pixelvergleich**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10-t5" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10-t5" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-t5-diff"`
|
||||
Expected: Gate Exit 0, Vergleich Exit 0. Auch alle Alt-Dialoge (`dlg-*`), die `.seg`-Ansichten (`finanzen`, `serverpuls`) und Links bleiben unverändert. Weicht eine Ansicht ab, wird das Diff-Bild angesehen. Es gibt zwei Ursachen:
|
||||
- Die gelöschte Regel gehört an die Stelle ihres Bausteins (Step 4).
|
||||
- Bereichs-CSS (`chat.css`, `serverpuls.css`, `pages/AdminPage.css`) lädt vor `global.css` und wurde bisher von KOMPAT überstimmt. Der Bereichs-Selektor bekommt dann eine höhere Spezifität, nie `!important`.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
Abschnitt 2 der Ergebnisdatei ist gefüllt.
|
||||
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
git diff --name-only
|
||||
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md tests/unit/hygiene.test.mjs src/styles/global.css <weitere geprüfte Dateien>
|
||||
git commit -m "Aufraeumen: KOMPAT-Block, .page-Verschachtelungsregel und Altklassen entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Tote Klassen und Keyframes
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/*.css`, `src/pages/*.css`, JSX-Dateien mit Klassen ohne Regel, Ergebnisdatei Abschnitt 3, `$W/css-klassen.mjs` (Ausnahmeliste)
|
||||
- Delete: CSS-Dateien ohne verbleibende Regel samt Importzeilen
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `css-klassen.mjs` mit `--ohne-ausnahmen` (Task 1).
|
||||
- Produces: `dead` und `undefinedUsed` sind leer, die Ausnahmeliste enthält nur Namen mit belegter Konstruktionsstelle.
|
||||
|
||||
- [ ] **Step 1: Liste erzeugen**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
node "$W/css-klassen.mjs" . --ohne-ausnahmen > "$TMPDIR/klassen-p10.json"; node -e "const j=JSON.parse(require('fs').readFileSync(process.argv[1]));for(const k of ['dead','undefinedUsed','nurAusnahme'])console.log(k, j[k].length+':', j[k].join(' '))" "$TMPDIR/klassen-p10.json"
|
||||
```
|
||||
Expected: drei Zeilen mit Namen. `dead` enthält zunächst auch die Namen aus `nurAusnahme`.
|
||||
|
||||
- [ ] **Step 2: Gegenprobe und Löschen**
|
||||
|
||||
Für jeden Namen aus `dead`, der nicht in `nurAusnahme` steht:
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "(^|[^A-Za-z0-9_-])NAME([^A-Za-z0-9_-]|$)" src --glob '!*.css'`
|
||||
Expected: keine Treffer. Dann fällt jede Regel mit diesem Namen in der definierenden Datei, in einer Selektorliste nur der Name.
|
||||
|
||||
Für jeden Namen aus `nurAusnahme` bekommt Abschnitt 3 der Ergebnisdatei eine Zeile mit der Stelle, die ihn baut: ein Template-Präfix wie `fin-fixed-${`, eine Zeichenkette in `lib/*.js` oder der Fremd-Composer bei `chat-*`. Ohne Beleg gilt die Klasse als tot und fällt.
|
||||
|
||||
- [ ] **Step 3: Klassen ohne Regel, `size`, Keyframes**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
rg -n 'size="sm"' src; rg -n '(^|[^A-Za-z0-9_-])(btn-sm|p-4|status-filter|supporters-editor|timeline)([^A-Za-z0-9_-]|$)' src --glob '!*.css'
|
||||
rg -o "@keyframes [A-Za-z0-9_-]+" src --glob '*.css' | sed 's/.*@keyframes //' | sort -u | while read k; do rg -q "animation[^;]*(^|[^A-Za-z0-9_-])${k}([^A-Za-z0-9_-]|;)" src --glob '*.css' || echo "TOT $k"; done
|
||||
```
|
||||
Expected: alle drei Ausgaben leer. Treffer der ersten beiden Befehle werden aus dem JSX entfernt (die Klassen haben keine Regel, `size` bewirkt nichts). Ein toter Keyframe fällt samt Regel. Zu erwarten ist `spin`, wenn `.spin` (Notiz aus P8) und `.spinner` keinen Aufrufer mehr haben.
|
||||
|
||||
- [ ] **Step 4: Leere Dateien, Ausnahmeliste, Schriftordner**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && wc -l src/styles/*.css src/pages/*.css; rg -n "admin-areas" src index.html; ls src/assets 2>&1 | head -1; rg -n -i "assets/fonts|lato" src index.html`
|
||||
Expected:
|
||||
- Eine CSS-Datei ohne Regel wird mit `git rm` gelöscht, ihre `import`-Zeilen verschwinden (`rg -l "<Dateiname>" src`). `admin-areas.css` hat P6 schon gelöscht, ein Treffer von `rg -n "admin-areas" src index.html` ist ein Rest und wird entfernt.
|
||||
- `src/assets` existiert nicht, das letzte `rg` hat keine Treffer.
|
||||
- Die Ausnahmeliste in `$W/css-klassen.mjs` enthält nur noch die Namen mit Beleg aus Step 2. `badge-*`, `status-*`, `ticket-tab-active`, `ui-btn-*` und `side-dot-*` fallen.
|
||||
|
||||
- [ ] **Step 5: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" . | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.length, j.undefinedUsed.length)" && "$W/check.sh" && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10-t6" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10-t6" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-t6-diff"`
|
||||
Expected: `0 0`, `check.sh` Exit 0 mit `# fail 0` und `# pass` = `N9 + 2`, Gate Exit 0, Vergleich Exit 0.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
Abschnitt 3 der Ergebnisdatei hat die Tabelle: Klasse, definierende Datei, Beleg oder „gelöscht“.
|
||||
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
git status --short
|
||||
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md <die geprüften Dateien>
|
||||
git commit -m "Aufraeumen: tote Klassen, Keyframes und leere CSS-Dateien geloescht, Klassen ohne Regel aus dem JSX entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: Inline-Styles, Regelprüfung, Invarianten
|
||||
|
||||
**Files:**
|
||||
- Modify: JSX-Dateien mit Inline-Styles ohne Laufzeitwert (Ersatz sind vorhandene Klassen, neue kommen nicht dazu), Ergebnisdatei Abschnitte 4, 5 und 7
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `$W/farben.mjs` (P1) und der Abschnitt „Bewusst offen“ in `docs/superpowers/plans/2026-10-05-weisses-design-p1-tote-klassen.md`.
|
||||
- Produces: eine begründete Liste aller `style=`-Stellen und die Regelprüfung der Spec als belegte Liste.
|
||||
|
||||
- [ ] **Step 1: Inline-Styles nur noch für Laufzeitwerte**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -nU "style=\{\{(\s*[A-Za-z]+:\s*('[^']*'|\"[^\"]*\"|-?[0-9]+(\.[0-9]+)?)\s*,?)+\s*\}\}" src --glob '*.jsx'`
|
||||
Expected: keine Treffer. Das Muster findet jedes Objekt, dessen Werte alle feste Zahlen oder Zeichenketten sind. Jeder Treffer wird durch eine vorhandene Klasse ersetzt.
|
||||
|
||||
- [ ] **Step 2: Rest begründen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "style=\{" src --glob '*.jsx'`
|
||||
Expected: wenige Zeilen. Abschnitt 4 der Ergebnisdatei bekommt je Zeile Datei:Zeile, Ausdruck und Begründung. Erlaubt sind ein aus Daten oder Messung berechneter Wert (Breite in Prozent, Höhe, Spaltenzahl) und das Durchreichen der `style`-Prop in `Badge.jsx`. Feste Abstände, Farben, Schrift, Flex und Größen sind nicht erlaubt, solche Stellen werden umgestellt, und Step 1 beginnt von vorn.
|
||||
|
||||
- [ ] **Step 3: Farben**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' src --glob '*.jsx' --glob '*.js'; node "$W/farben.mjs" .`
|
||||
Expected:
|
||||
- Der `rg` nennt nur `src/components/Logo.jsx` und `src/components/serverpuls/chartColors.js`.
|
||||
- `farben.mjs` listet kein Literal unter 3:1 auf Weiß, keine dunkle Schrift auf `--accent` und kein Weiß-Alpha.
|
||||
- Jede Zeile des Abschnitts „Bewusst offen“ in `p1-tote-klassen.md` ist erledigt. Hat sie das Zielpaket P10, wird sie hier behoben und in Abschnitt 5 genannt.
|
||||
|
||||
- [ ] **Step 4: Regelprüfung der Spec**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
rg -n "!important" src # 1
|
||||
rg -n -e "gradient\(" -e "backdrop-filter" -e "filter: *blur" src --glob '*.css' # 2
|
||||
rg -n "text-transform" src --glob '*.css' # 3
|
||||
rg -n "font-weight: *(800|900)" src # 4
|
||||
rg -n -e "Lato" -e "@font-face" src index.html # 5
|
||||
rg -n "font-family" src --glob '*.css' # 6
|
||||
rg -n -e "color-scheme" -e "data-theme" -e "data-accent" src index.html # 7
|
||||
rg -no -e "@media[^{]*" -e "@container[^{]*" src --glob '*.css' # 8
|
||||
rg -n "<svg" src --glob '*.jsx' # 9
|
||||
rg -n -e "Kpi" -e "kpi" src # 10
|
||||
rg -n -e "<title>" -e 'rel="icon"' index.html # 11
|
||||
for s in '\.num' '\.stat \.v' '\.fin-amount'; do printf '%s: ' "$s"; rg -l -U "$s([^A-Za-z0-9_{}-][^{}]*)?\{[^}]*tabular-nums" src --glob '*.css' | tr '\n' ' '; echo; done # 12
|
||||
```
|
||||
Expected:
|
||||
- 1, 4, 5 und 10: leer.
|
||||
- 2: höchstens eine Zeile, der Chevron von `.select`. 3: eine Zeile, die Wochentage `.wd` im Planboard.
|
||||
- 6: nur `var(--font)` und der Monospace-Stapel (`ui-monospace`). 7: nur `color-scheme: light`.
|
||||
- 8: nur `max-width: 1100px` und `860px`, bei `@container` 560, 640 und 860 px, dazu `prefers-reduced-motion`. Zugelassen ist außerdem der eine Block `max-width: 640px` in `src/styles/chat.css`: Er setzt im Eingabefeld des fremden Portal-Composers wieder `font-size: 16px` (gegen das iOS-Zoomen, P6) und ist die WOMS-eigene Überschreibung des Composers.
|
||||
- 9: nur `Logo.jsx` und `Sparkline.jsx`. 11: `Webklar WOMS 2.0` und die Zeile für `/favicon.svg`.
|
||||
- 12 (Spec-Regel 6, Zahlen und Beträge): drei Zeilen, jede nennt mindestens `src/styles/global.css`. Fehlt bei einem Selektor die Datei, trägt seine Regel kein `font-variant-numeric: tabular-nums`. P10 ergänzt dann genau diese Deklaration an der bestehenden Regel. Das ist die einzige erlaubte Abweichung von der Pixelinvariante. Die Ansichten, die dadurch im Vergleich von Task 8 abweichen, stehen mit Begründung in Abschnitt 5.
|
||||
- Spec-Regel 4 (höchstens ein schwarzer Knopf je Seitenkopf) prüft kein `rg`, sondern der Check `max-primary` im Gate `p10` (Task 1 Step 6, Lauf in Task 8 Step 2).
|
||||
|
||||
Seitenbreakpoints außerhalb von 860 und 1100 px werden auf diese Werte gezogen (Spec, Regel 7), ausgenommen der 640-px-Block in `chat.css`. Jede andere Abweichung steht mit Begründung in Abschnitt 7. Befehl, Ausgabe und Urteil jeder Zeile stehen in Abschnitt 5.
|
||||
|
||||
- [ ] **Step 5: Invarianten gegen `design-p0`**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
git grep -ho 'data-testid=[^ >]*' design-p0 -- src | sort -u > "$TMPDIR/ti-p0.txt"
|
||||
rg -oN --no-filename 'data-testid=[^ >]*' src | sort -u > "$TMPDIR/ti-jetzt.txt"
|
||||
diff "$TMPDIR/ti-p0.txt" "$TMPDIR/ti-jetzt.txt" | rg '^<'
|
||||
echo "confirm: $(git grep -h 'window\.confirm' design-p0 -- src | wc -l) -> $(rg --no-filename 'window\.confirm' src | wc -l)"
|
||||
git diff --name-status design-p0..HEAD -- src/hooks src/lib
|
||||
```
|
||||
Expected:
|
||||
- Das `diff` zeigt keine `<`-Zeile, es ging keine `data-testid` verloren. Neue (`>`) sind erlaubt und werden genannt.
|
||||
- Die Zahl der `window.confirm` ist gleich.
|
||||
- In `src/hooks` steht nur `M useWorkorders.js` plus die neuen `useWorkorder.js`, `useTicketCounts.js`, `useTicketMinutes.js`. In `src/lib` stehen nur `D sidebar.js`, neue Dateien (darunter `rowClick.js` aus P2) und `M` für `ticketStatus.js`, `navLinks.js`, `previewEnv.js`, `serverpulsFormat.js`, `creditFormat.js`, `offerFormat.js`, `storageFormat.js`, `projectList.js`, `leads.js`. Jede andere `M`- oder `D`-Zeile ist ein Befund.
|
||||
|
||||
Die Ausgaben stehen in Abschnitt 5.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-tests && git status --short`
|
||||
Expected: Exit 0, `# fail 0`, `# pass` = `N9 + 2`, nur die erwarteten Änderungen.
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md <die geprüften Dateien aus Step 1 bis 4>
|
||||
git commit -m "Aufraeumen: Inline-Styles nur noch fuer Laufzeitwerte, Regelpruefung und Invarianten dokumentiert" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: Gate P10, Bundle, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p9 HEAD && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: `design-p9` ist Vorfahre, der Rebase läuft ohne Konflikt, Exit 0 mit `# fail 0` und `# pass` = `N9 + 2`. Gate, Bilder und Review beziehen sich damit auf den Stand, der getaggt wird.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Lauf und Pixelvergleich**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p10 && "$W/shots.sh" p10 "$W/shots/p10" --gate p10 && "$W/run-browser.sh" "$W/vergleich.mjs" "$W/shots/p9-ref" "$W/shots/p10" --rauschen "$W/shots/p9-ref2" --out "$W/shots/p10-diff"`
|
||||
Expected:
|
||||
- Das Gate `p10` endet mit Exit 0. Über alle Ansichten, auch die der Review-Focus-Punkte 1 bis 5 der Übersicht, gibt es keine Verstöße gegen `external-request`, `console-error`, `dark-surface`, `low-contrast`, `overflow-x`, `icon-svg` (außer unter `.woms-chat`), `max-primary` und `step-failed`.
|
||||
- Der Vergleich endet mit Exit 0, `fehlt` ist leer, die Zahl der Ansichten gleicht der Sollzahl aus Task 1 Step 4. Rausch-Ansichten stehen in `$W/p10-rauschen.txt`.
|
||||
- Die Ansicht `alt-link` hat keinen Verstoß. Ihr Schritt prüft, dass `/tickets?woid=100131&status=done` auf `/tickets/100131` endet.
|
||||
|
||||
- [ ] **Step 3: Bundle-Größe**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&
|
||||
"$W/check.sh" --nur-build &&
|
||||
{ find dist -type f \( -name '*.js' -o -name '*.css' -o -name '*.ttf' \) -printf '%s %p\n'; for f in dist/assets/*.js dist/assets/*.css; do echo "$f gzip=$(gzip -9 -c "$f" | wc -c)"; done; wc -l src/styles/*.css src/pages/*.css; } | tee "$W/p10-baseline-p10.txt"
|
||||
```
|
||||
Expected:
|
||||
- Abschnitt 6 der Ergebnisdatei bekommt eine Tabelle mit den Zeilen JS in Byte, JS gzip, CSS in Byte, CSS gzip, Schrift in Byte, CSS-Zeilen und Testzahl und den Spalten P0, `design-p9`, P10. Die P0-Werte stammen aus der Commit-Zeile `Ausgangswerte` (Gegenprobe: Umbaukarte), die P9-Werte aus `$W/p10-baseline-p9.txt`.
|
||||
- CSS und CSS-Zeilen sind kleiner als bei `design-p9`, JS ist nicht größer. Gegenüber P0 fehlen 96.184 B Schrift. Werte, die gegenüber P0 wachsen, stehen in Abschnitt 7.
|
||||
|
||||
- [ ] **Step 4: Ergebnisdatei abschließen**
|
||||
|
||||
Abschnitt 7 (Offene Punkte) ist gefüllt, auch wenn er nur „keine“ sagt.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git status --short`
|
||||
Expected: nur `M docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md`.
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/plans/2026-10-05-weisses-design-p10-ergebnisse.md
|
||||
git commit -m "Aufraeumen: Ergebnisdatei mit Bundle-Vergleich und offenen Punkten" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p9..HEAD` gegen die Spec-Abschnitte „Übergangsschicht KOMPAT“, „Gestaltungsregeln“ und „Was gleich bleibt“, dazu liest der Prüfer die Ergebnisdatei. Schwerpunkte:
|
||||
- Der Diff enthält nur Löschungen, Umbenennungen und Umstellungen auf vorhandene Bausteine, keine neuen Texte, Klassen oder Tokens außer dem Wächtertest.
|
||||
- Zehn Stichproben aus Abschnitt 2 (KOMPAT) und 3 (tote Klassen): Die Klasse hat per `rg` keinen Nutzer, ihre Regel ist weg.
|
||||
- `package.json` und Lock haben genau `@tabler/icons-react`, `@tabler/icons` und `react-icons` verloren.
|
||||
- `git diff design-p9 -- src/pages/TicketsPage.jsx src/lib/ticketListUrl.js` ist leer oder enthält nur Kommentare, `woidRedirectTarget` ist erhalten.
|
||||
- Jede Begründung in Abschnitt 4 stimmt, die Invarianten aus Abschnitt 5 sind erfüllt.
|
||||
|
||||
Wichtige Befunde werden behoben, danach laufen Step 1 bis 3 erneut. Kleinigkeiten kommen in Abschnitt 7.
|
||||
|
||||
- [ ] **Step 6: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p10 && git log --oneline origin/test..design-p10 && git rev-parse design-p10`
|
||||
Expected: Exit 0, Tag gesetzt. Brachte der Rebase in Step 1 neue Commits mit, liefen Gate und Vergleich bereits auf dem rebaseten Stand.
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- den Bericht des Pixelvergleichs („0 abweichende Ansichten von <Zahl>“) und die Screenshots aus `$W/shots/p10/1440x900` und `$W/shots/p10/390x844` (Auswahl: `login`, `tickets`, `ticket`, `kunde-overview`, `projekte`, `finanzen`, `dlg-rechnung-kunde`, `serverpuls`, ein Alt-Dialog)
|
||||
- die Bundle-Tabelle aus Abschnitt 6 der Ergebnisdatei
|
||||
- den Hinweis, dass sichtbar nichts Neues erscheint, der Link `/tickets?woid=N` aus Portal-Mails weiter funktioniert und die offenen Punkte in Abschnitt 7 stehen
|
||||
- den Hinweis, dass damit alle Pakete ausgeliefert sind und Worktree `dev-design` samt Branch `feature/design-weiss` danach entfernt werden können, wenn er das will
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p10^{commit}:refs/heads/test'`
|
||||
- die Rücknahme `git revert --no-commit design-p9..design-p10` (Commit und Push auf `test` durch den Eigentümer, gilt nur solange P10 das jüngste Paket ist)
|
||||
|
||||
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“. Dort läuft `npm install` mit dem neuen Lock durch.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||||
- `curl -s -o /dev/null -w '%{http_code}' 'https://ticket.webklar.com/tickets?woid=1'` liefert 200 (Seiten-Fallback).
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers, ohne Schreibaktion: Ein alter Link `/tickets?woid=<N>` mit einer echten Nummer landet auf `/tickets/<N>`. Ticketliste, ein Ticket, das Menü mit dem Serverpuls-Punkt und ein Dialog (öffnen, mit Esc schließen) sehen aus wie vor dem Push, und die Konsole zeigt keinen Fehler.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,441 @@
|
||||
# WOMS „Weißes Design“ P3: Planboard – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P3 zeigt das Planboard eine Wochenansicht ohne Karten:
|
||||
- sieben Tagesspalten mit Haarlinien, Wochenende in `--bg-subtle`, heutiger Tag als schwarze Datumspille
|
||||
- Termine als Links auf die Ticketseite, Akquise-Termine hell orange, stornierte Termine weiter sichtbar
|
||||
- Kopf „Kalenderwoche N · Datum bis Datum · n Termine“ mit den Aktionen ← · Heute · →
|
||||
- unter 860 px eine Spalte
|
||||
|
||||
Die Logik in `lib/planboard.js` bleibt unverändert.
|
||||
|
||||
**Architecture:**
|
||||
- `.week`, `.day`, `.appt` und ihre Unterklassen kommen als eigener Block „Planboard (P3)“ in `src/styles/global.css`, vor den KOMPAT-Block. Die Werte stammen aus `quelle/styles.css` (Zeilen 379–397 und 535–537).
|
||||
- Die reine Anzeigelogik (Kopfzeile, Terminzeile, Klassen, Linkziel) steht in der neuen `src/lib/planboardView.js` mit Tests. `PlanboardPage.jsx` besteht danach nur noch aus Markup.
|
||||
- Die Seite nutzt `PageHeader` und `Button` aus P1 sowie `ticketPath` und `ticketRef` aus P2. Sie enthält keine Icons und keine Inline-Styles.
|
||||
|
||||
**Tech Stack:** React 18, react-router-dom 6, date-fns 2.30 (`de`, `getISOWeek`), reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0 (Playwright 1.62, `page.clock`).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Seitenkopf, Knöpfe, Links), „Ticket als eigene Seite / Route und Links“, „Texte“ und Paket P3. Detail im Anhang `2026-10-04-weisses-design-umbaukarte.md`, Abschnitt 5 (P3), 3 und 8.1 (Punkt 8). Globale Regeln und gemeinsame Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, aktuelles Verzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"` (Regeln der Übersicht).
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P3 kommt hinzu:
|
||||
|
||||
- Zeilenangaben gelten für den Stand `603d08c`. Nach P1 bis P2 können sie leicht verschoben sein, maßgeblich sind die genannten Namen.
|
||||
- Voraussetzung ist das Tag `design-p2` mit `src/lib/ticketRoutes.js` (`ticketPath`, `ticketRef`).
|
||||
- Geändert werden nur `src/pages/PlanboardPage.jsx` und `src/styles/global.css` (ein neuer Block). Neu sind `src/lib/planboardView.js` und zwei Testdateien. `src/lib/planboard.js`, `useWorkorders` und `useEmployees` bleiben unverändert.
|
||||
- Sichtbare Texte bleiben wörtlich: „Planboard“, „Heute“, „Keine Termine“, „ohne Uhrzeit“, „niemand zugewiesen“, Tooltip „Ticket #N öffnen“. Neu ist nur, was die Spec unter „Planboard-Kopf“ nennt: die Kopfzeile. Die `aria-label`-Texte „Vorherige Woche“ und „Nächste Woche“ sind nicht sichtbar.
|
||||
- **Erreichbar bleiben** alle heutigen Bedienelemente: Wochenwechsel (zwei Pfeilknöpfe), „Heute“ und jeder Termin als Link auf sein Ticket. Es gibt keine `data-testid` und keine `window.confirm` in der Seite.
|
||||
- **Entscheidung:** Der Tageskopf behält die heutigen Texte (voller Wochentag, Datum als `dd.MM`). Die Spec ist dort still, der Prototyp kürzt auf „Mo 5.“. Die Großbuchstaben des Wochentags setzt das CSS (Spec). Weil die Spalten bei 861–1100 px unter 120 px breit sind, darf `.day-head` umbrechen. Das volle Datum steht zusätzlich im `aria-label` der Spalte.
|
||||
- **Entscheidung:** Kein Fußtext und keine Legende „Akquise“. Der Prototyp hat beides, die Spec nennt es nicht.
|
||||
- **Entscheidung:** Stornierte Termine sind sichtbar und zählen im Kopf mit, ohne eigene Optik (wie heute).
|
||||
- **Entscheidung:** Das Linkziel ist `ticketPath(ticketRef({ woid: entry.woid, $id: entry.id }))`, also die `woid` und ohne sie die `$id`. Die Zeile eines Termins ohne `woid` zeigt kein leeres „#“ mehr, sondern nur den Mitarbeiter, sein Tooltip lautet „Ticket öffnen“.
|
||||
- **Entscheidung:** Während die Tickets laden, endet die Kopfzeile auf „Wird geladen …“ statt auf einer Terminzahl.
|
||||
- **Entscheidung:** Akquise wird wie heute über `String(type).toLowerCase() === 'akquise'` erkannt. Die grünen und grauen Typfarben entfallen, alle anderen Termine bekommen `--bg-hover`.
|
||||
- **Entscheidung:** Der Inhalt eines Termins behält die heutige Reihenfolge (Zeit, Kunde oder Betreff, Betreff, Nummer · Mitarbeiter), damit keine Information wandert.
|
||||
- **Entscheidung:** Auf Wochenendspalten bekommen Termine einen weißen Grund, sonst verschwinden sie im `--bg-subtle`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Alle fünf Punkte der Übersicht treffen das Planboard. Jede Prüfung hat eine Ansicht in Task 4.
|
||||
|
||||
1. **Altdaten:** Ansicht `planboard-altdaten`.
|
||||
- `2.5.2026` erscheint am Samstag der Woche 18.
|
||||
- `31.02.2026`, ein ISO-Datum und ein leeres Datum ergeben weder einen Eintrag noch einen Fehler.
|
||||
- Ein Ticket ohne `woid` verlinkt über die `$id` und zeigt kein leeres „#“.
|
||||
- Im Text steht weder `undefined` noch `null` noch `NaN`.
|
||||
2. **Lange Inhalte:** Ansicht `planboard`. Am Samstag stehen ein Kundenname mit 60 und ein Betreff mit 120 Zeichen. Es gibt kein waagerechtes Scrollen (`overflow-x`), und kein `.day` ist breiter als seine Spalte (`scrollWidth <= clientWidth`).
|
||||
3. **Teilausfall:** Ansicht `planboard-fehler`. Antwortet die Mitarbeiterliste mit 500, erscheinen alle 13 Termine. Der Mitarbeiter steht dann aus `assignedName`.
|
||||
4. **Nur Tastatur:** Ansicht `planboard-tastatur`. Der erste Termin ist per Tab erreichbar, trägt den schwarzen Fokusring und öffnet mit Enter `/tickets/100201`.
|
||||
5. **Viele Einträge:** Der Donnerstag hat sechs Termine. Die Spalte wächst nach unten, nichts überlappt, die anderen Spalten bleiben unverändert hoch.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Paketstart und Ausgangswerte
|
||||
|
||||
**Files:** keine Änderung (nur Lesen und Prüfen).
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tag `design-p2`, `ticketPath(ref, tab)` und `ticketRef(ticket)` aus `src/lib/ticketRoutes.js`.
|
||||
- Produces: die Ausgangs-Testzahl N0 für die Übergabe.
|
||||
|
||||
- [ ] **Step 1: Paketstart**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p2 HEAD && git rebase origin/test && "$W/check.sh" && git status --short`
|
||||
Expected:
|
||||
- Alle Befehle enden mit Exit 0, `git status` gibt keine Zeile aus. Schlägt `merge-base` fehl, ist `design-p2` nicht im Branch: Stopp und melden, P3 darf erst nach P2 laufen.
|
||||
- `check.sh` zeigt `# fail 0`. Die Zahl `# pass` ist N0 (Soll nach P3: N0 + 9).
|
||||
|
||||
- [ ] **Step 2: Parallele Arbeit an `feature/terminplaner-tp2`**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/PlanboardPage.jsx src/lib/planboard.js src/lib/planboardView.js src/styles/global.css`
|
||||
Expected: keine Ausgabe. Gibt der Befehl Zeilen aus, stoppt der Agent und fragt den Eigentümer. Er empfiehlt, tp2 zuerst nach `test` zu bringen.
|
||||
|
||||
- [ ] **Step 3: `ticketPath` und `ticketRef` sind da**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "export function (ticketPath|ticketRef)" src/lib/ticketRoutes.js`
|
||||
Expected: zwei Treffer. Sonst Stopp: P3 darf erst nach P2 laufen.
|
||||
|
||||
- [ ] **Step 4: Stand der Seite und des CSS**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath|react-icons|style=\{\{" src/pages/PlanboardPage.jsx; rg -n "^\s*\.(week|day|appt)\b" src/styles/global.css`
|
||||
Expected:
|
||||
- In der Seite: ein Treffer `ticketPath` (P2 hat die Linkzeile umgestellt), dazu die Icon-Importe und die Inline-Styles.
|
||||
- Im CSS: keine Treffer. Findet P1 dort schon Mobil-Regeln für `.week` oder `.day`, werden sie in Task 5 Step 1 in den P3-Block verschoben.
|
||||
|
||||
### Task 2: Charakterisierungstests für `lib/planboard.js`
|
||||
|
||||
Die Logik hat bisher keinen Test. P3 benutzt sie unverändert, die Altwerte aus Review Focus 1 werden jetzt festgeschrieben.
|
||||
|
||||
**Files:**
|
||||
- Test: `tests/unit/planboard.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `parseGermanDate`, `formatTime`, `dayKey`, `groupAppointmentsByDay`, `countInRange` aus `src/lib/planboard.js`, unverändert.
|
||||
- Produces: vier Tests.
|
||||
|
||||
- [ ] **Step 1: Tests schreiben**
|
||||
|
||||
```js
|
||||
const { parseGermanDate, formatTime, dayKey, groupAppointmentsByDay, countInRange } = await loadModule('src/lib/planboard.js')
|
||||
|
||||
test('parseGermanDate: ein- und zweistellig, Altwerte ergeben null', () => {
|
||||
assert.equal(parseGermanDate('2.5.2026').getTime(), new Date(2026, 4, 2).getTime())
|
||||
assert.equal(parseGermanDate(' 07.10.2026 ').getTime(), new Date(2026, 9, 7).getTime())
|
||||
for (const v of ['31.02.2026', '0.1.2026', '2026-10-07T09:00:00.000Z', '2026-10-07', '7.10.26', '', undefined, null, 'morgen']) {
|
||||
assert.equal(parseGermanDate(v), null, String(v))
|
||||
}
|
||||
})
|
||||
|
||||
test('formatTime: hhmm und hh:mm, ungültige Werte ergeben leer', () => {
|
||||
for (const [v, out] of [['0900', '09:00'], ['900', '09:00'], ['9:00', '09:00'], ['09:30', '09:30'], ['2359', '23:59']]) {
|
||||
assert.equal(formatTime(v), out, v)
|
||||
}
|
||||
for (const v of ['2400', '1260', '9', 'abc', '', undefined]) assert.equal(formatTime(v), '', String(v))
|
||||
assert.equal(dayKey(new Date(2026, 0, 5)), '2026-01-05')
|
||||
})
|
||||
|
||||
const employees = [{ userId: 'u1', displayName: 'Kenso Grimm', shortcode: 'KNSO' }]
|
||||
const workorders = [
|
||||
{ $id: 'a', woid: '1', startDate: '7.10.2026', startTime: '1400', endTime: '1500', topic: 'Spät', type: 'Webpage', status: 'Open', customerName: 'Alpha', assignedTo: 'u1' },
|
||||
{ $id: 'b', woid: '2', startDate: '07.10.2026', startTime: '', topic: 'Ohne Uhrzeit', type: 'Akquise', status: 'Open', assignedName: 'Ehemalige' },
|
||||
{ $id: 'c', woid: '3', startDate: '7.10.2026', startTime: '0830', topic: 'Früh', type: 'Webpage', status: 'Cancelled' },
|
||||
{ $id: 'd', startDate: '8.10.2026', startTime: '1000', title: 'Nur Titel' },
|
||||
{ $id: 'h', startDate: '9.10.2026' },
|
||||
{ $id: 'e', woid: '5', startDate: '31.02.2026', startTime: '1000', topic: 'Ungültig' },
|
||||
{ $id: 'f', woid: '6', startDate: '2026-10-07T09:00:00.000Z', topic: 'ISO' },
|
||||
{ $id: 'g', woid: '7', startDate: '', topic: 'Leer' },
|
||||
]
|
||||
|
||||
test('groupAppointmentsByDay: nach Uhrzeit, ohne Uhrzeit am Ende, Stornierte bleiben, Altwerte fallen weg', () => {
|
||||
const byDay = groupAppointmentsByDay(workorders, employees)
|
||||
assert.deepEqual(Object.keys(byDay).sort(), ['2026-10-07', '2026-10-08', '2026-10-09'])
|
||||
assert.deepEqual(byDay['2026-10-07'].map((a) => a.id), ['c', 'a', 'b'])
|
||||
assert.equal(byDay['2026-10-07'][0].status, 'Cancelled')
|
||||
assert.equal(byDay['2026-10-07'][1].employeeShort, 'KNSO')
|
||||
assert.equal(byDay['2026-10-07'][1].employeeName, 'Kenso Grimm')
|
||||
assert.equal(byDay['2026-10-07'][2].employeeName, 'Ehemalige')
|
||||
assert.equal(byDay['2026-10-07'][2].employeeShort, '')
|
||||
assert.equal(byDay['2026-10-08'][0].woid, '')
|
||||
assert.equal(byDay['2026-10-08'][0].topic, 'Nur Titel')
|
||||
assert.equal(byDay['2026-10-09'][0].topic, 'Ohne Betreff')
|
||||
})
|
||||
|
||||
test('countInRange: zählt nur die genannten Tage', () => {
|
||||
const byDay = groupAppointmentsByDay(workorders, employees)
|
||||
const week = (day) => Array.from({ length: 7 }, (_, i) => new Date(2026, 9, day + i))
|
||||
assert.equal(countInRange(byDay, week(5)), 5)
|
||||
assert.equal(countInRange(byDay, week(12)), 0)
|
||||
assert.equal(countInRange(byDay), 0)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Tests laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboard.test.mjs`
|
||||
Expected: PASS, 4 Tests. Die Tests halten den Bestand fest, es ändert sich kein Produktivcode. Schlägt einer fehl, wurde das Verhalten falsch gelesen. Dann wird der Test korrigiert, nicht `planboard.js`.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/unit/planboard.test.mjs
|
||||
git commit -m "Planboard: Tests halten Datums- und Terminlogik fest (Altwerte, Stornierte, ohne Uhrzeit)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: `planboardView.js` (Anzeigelogik)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/planboardView.js`
|
||||
- Test: `tests/unit/planboardView.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `date-fns` (`format`, `getISOWeek`, `addDays`) und `de`.
|
||||
- Produces (Signaturen, die `PlanboardPage` benutzt):
|
||||
- `planboardSubtitle({ weekStart: Date, count: number, loading?: boolean }) → string`
|
||||
- `dayLabels(day: Date) → { weekday: string, number: string, full: string }`
|
||||
- `appointmentClass(type) → 'appt' | 'appt akquise'`
|
||||
- `appointmentTimeLabel({ time, endTime }) → string`
|
||||
- `appointmentOwnerLabel({ woid, employeeName, employeeShort }) → string`
|
||||
- `appointmentTitle({ woid }) → string`
|
||||
|
||||
- [ ] **Step 1: Failing Tests schreiben**
|
||||
|
||||
```js
|
||||
const { planboardSubtitle, dayLabels, appointmentClass, appointmentTimeLabel, appointmentOwnerLabel, appointmentTitle } =
|
||||
await loadModule('src/lib/planboardView.js')
|
||||
|
||||
test('planboardSubtitle: Kalenderwoche, Zeitraum, Terminzahl', () => {
|
||||
const mo = new Date(2026, 9, 5)
|
||||
assert.equal(planboardSubtitle({ weekStart: mo, count: 13 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 13 Termine')
|
||||
assert.equal(planboardSubtitle({ weekStart: mo, count: 1 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 1 Termin')
|
||||
assert.equal(planboardSubtitle({ weekStart: mo, count: 0 }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 0 Termine')
|
||||
assert.equal(planboardSubtitle({ weekStart: mo, count: 0, loading: true }), 'Kalenderwoche 41 · 5. Oktober bis 11. Oktober · Wird geladen …')
|
||||
assert.equal(planboardSubtitle({ weekStart: new Date(2026, 11, 28), count: 2 }), 'Kalenderwoche 53 · 28. Dezember bis 3. Januar · 2 Termine')
|
||||
assert.equal(planboardSubtitle({ weekStart: new Date(2027, 0, 4), count: 2 }), 'Kalenderwoche 1 · 4. Januar bis 10. Januar · 2 Termine')
|
||||
})
|
||||
|
||||
test('dayLabels: Wochentag, Datum, volles Datum', () => {
|
||||
assert.deepEqual(dayLabels(new Date(2026, 9, 5)), { weekday: 'Montag', number: '05.10', full: 'Montag, 5. Oktober' })
|
||||
assert.deepEqual(dayLabels(new Date(2026, 9, 11)), { weekday: 'Sonntag', number: '11.10', full: 'Sonntag, 11. Oktober' })
|
||||
})
|
||||
|
||||
test('appointmentClass und appointmentTimeLabel', () => {
|
||||
for (const t of ['Akquise', 'akquise', 'AKQUISE']) assert.equal(appointmentClass(t), 'appt akquise', t)
|
||||
for (const t of ['Webpage', 'Project', 'Migration', '', undefined, null]) assert.equal(appointmentClass(t), 'appt', String(t))
|
||||
assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '09:30' }), '08:00–09:30')
|
||||
assert.equal(appointmentTimeLabel({ time: '08:00', endTime: '' }), '08:00')
|
||||
assert.equal(appointmentTimeLabel({ time: '', endTime: '09:30' }), 'ohne Uhrzeit')
|
||||
assert.equal(appointmentTimeLabel({ time: '', endTime: '' }), 'ohne Uhrzeit')
|
||||
})
|
||||
|
||||
test('appointmentOwnerLabel: Nummer und Mitarbeiter', () => {
|
||||
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), '#100201 · KNSO')
|
||||
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: 'Ehemalige', employeeShort: '' }), '#100201 · Ehemalige')
|
||||
assert.equal(appointmentOwnerLabel({ woid: '100201', employeeName: '', employeeShort: '' }), '#100201 · niemand zugewiesen')
|
||||
assert.equal(appointmentOwnerLabel({ woid: '', employeeName: 'Kenso Grimm', employeeShort: 'KNSO' }), 'KNSO')
|
||||
assert.equal(appointmentOwnerLabel({ woid: '', employeeName: '', employeeShort: '' }), 'niemand zugewiesen')
|
||||
})
|
||||
|
||||
test('appointmentTitle: Tooltip mit und ohne woid', () => {
|
||||
assert.equal(appointmentTitle({ woid: '100201' }), 'Ticket #100201 öffnen')
|
||||
assert.equal(appointmentTitle({ woid: '' }), 'Ticket öffnen')
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboardView.test.mjs`
|
||||
Expected: FAIL, `Could not resolve "src/lib/planboardView.js"` bzw. Funktionen fehlen.
|
||||
|
||||
- [ ] **Step 3: `planboardView.js` implementieren**
|
||||
|
||||
Festgelegte Werte:
|
||||
- Zeitraum: `format(d, 'd. MMMM', { locale: de })` für `weekStart` und `addDays(weekStart, 6)`, Verbindung „ bis “.
|
||||
- Kopfzeile: `Kalenderwoche ${getISOWeek(weekStart)} · ${von} bis ${bis} · ${rest}`. `rest` ist `Wird geladen …` bei `loading`, sonst `${count} Termin` (genau 1) oder `${count} Termine`.
|
||||
- `dayLabels`: `weekday` = `format(day, 'EEEE', { locale: de })`, `number` = `format(day, 'dd.MM')`, `full` = `format(day, 'EEEE, d. MMMM', { locale: de })`.
|
||||
- Zeitbereich in `appointmentTimeLabel` mit Halbgeviertstrich „–“ (U+2013), wie heute.
|
||||
- Die Datei importiert `date-fns`. Der Test-Lader bündelt es (geprüft mit `getISOWeek` und `date-fns/locale`), sie darf nur keinen Appwrite-Import bekommen.
|
||||
- `appointmentOwnerLabel`: wie heute entscheidet `employeeName`, ob ein Name oder „niemand zugewiesen“ erscheint. Mit `woid` steht `#woid · …`, ohne `woid` nur der Teil nach dem Punkt.
|
||||
|
||||
- [ ] **Step 4: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/planboardView.test.mjs && "$W/check.sh" --nur-tests`
|
||||
Expected: PASS, Exit 0, `# fail 0`, `# pass` = N0 + 9.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/planboardView.js tests/unit/planboardView.test.mjs
|
||||
git commit -m "Planboard: Anzeigelogik fuer Kopfzeile, Tageskopf und Terminzeile mit Tests" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 4: Testdaten, Ansichten und Gate (Prüfung vor dem Umbau)
|
||||
|
||||
Alles liegt im Werkzeug außerhalb des Repos, deshalb gibt es keinen Commit.
|
||||
|
||||
**Files** (unter `$W/`):
|
||||
- Create: `fixtures/planboard.mjs`
|
||||
- Modify: `views.mjs`, `gates.mjs`, bei Bedarf `backend.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createBackend`, `VIEWS`, `GATES` aus P0.
|
||||
- Produces: die Ansichten `planboard`, `planboard-woche-vor`, `planboard-leer`, `planboard-altdaten`, `planboard-fehler`, `planboard-tastatur` und das Gate `p3`.
|
||||
|
||||
- [ ] **Step 1: `fixtures/planboard.mjs` anlegen**
|
||||
|
||||
Aufbau wie `altdaten.mjs`: Es importiert `standard.mjs` und ersetzt `workorders` durch genau diese Tickets. Alle zugewiesenen Tickets tragen zusätzlich `assignedName` (voller Name). Ein Kürzel steht für `assignedTo` = `userId` des Mitarbeiters mit diesem Kürzel aus `standard`. Tickets „ohne `woid`“ haben kein Feld `woid` und die feste `$id` aus der Tabelle.
|
||||
|
||||
| woid | startDate | Zeit | Art | Status | Kunde | Betreff | Zuständig |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 100201 | `5.10.2026` | 0900–1030 | Webpage | Occupied | Bäckerei Sonnenschein | Öffnungszeiten für die Feiertage anpassen | KNSO |
|
||||
| 100202 | `06.10.2026` | 1400, ohne Ende | Akquise | In Kontakt | Gärtnerei Grünblick | Erstgespräch Rechnungsanalyse | ANDJ |
|
||||
| 100203 | `7.10.2026` | 0830–0915 | Webpage | Occupied | Kanzlei Weber & Partner | Kontaktformular landet im Spam | JUSN |
|
||||
| 100204 | `7.10.2026` | ohne Zeit | Migration | Open | ohne Kunde | Umzug ohne Uhrzeit und ohne Kunde | niemand, ohne `assignedName` |
|
||||
| 100205 | `7.10.2026` | 1500–1600 | Webpage | Cancelled | Fahrschule Roth | Storniertes Gespräch | KNSO |
|
||||
| 100211–100216 | `8.10.2026` | 0800, 0900, 1000, 1300, 1400, 1530 | 100214 ist Akquise, sonst Webpage | gemischt | Testkunde 1–6 | Termin 1–6 | gemischt |
|
||||
| ohne `woid`, `$id: 'ohne-woid-fr'` | `9.10.2026` | 1100–1115 | Project | Open | Physiotherapie am Markt | Ticket ohne Ticketnummer | NIRE |
|
||||
| 100206 | `10.10.2026` | 1000–1100 | Webpage | Occupied | `Müller-Lüdenscheidt Metallbau- und Anlagentechnik GmbH & Co.` (60 Zeichen) | Betreff mit 120 Zeichen (Wert aus `altdaten.mjs`) | `assignedTo: 'ehemalig'` (nicht in der Mitarbeiterliste), `assignedName: 'Ehemalige Kollegin'` |
|
||||
| 100230 | `12.10.2026` | 0900–1000 | Webpage | Open | Café Mühlental | Termin nächste Woche | JUSN |
|
||||
|
||||
Dazu die Altwerte, die nie im Planboard erscheinen dürfen oder in einer anderen Woche liegen:
|
||||
|
||||
| woid | startDate | Zeit | Betreff | Erwartung |
|
||||
|---|---|---|---|---|
|
||||
| 100301 | `2.5.2026` | 1000–1100 | Altwert einstellig (Kunde „Altkunde GmbH“, KNSO) | erscheint am Samstag der Woche 18 |
|
||||
| ohne `woid`, `$id: 'ohne-woid-alt'` | `1.5.2026` | 0900 | Ticket ohne Nummer im Altbestand (Altkunde GmbH, niemand) | erscheint am Freitag der Woche 18 |
|
||||
| 100302 | `31.02.2026` | 0900 | Ungültiges Datum | nie sichtbar |
|
||||
| 100303 | `2026-10-07T09:00:00.000Z` | – | ISO-Datum | nie sichtbar |
|
||||
| 100304 | leer | – | Ohne Datum | nie sichtbar |
|
||||
|
||||
Soll: In der Woche 5.–11. Oktober stehen 13 Termine (1 + 1 + 3 + 6 + 1 + 1), davon 2 Akquise (100202, 100214). In der Woche 12.–18. Oktober steht 1 Termin, in der Woche 27. April–3. Mai stehen 2.
|
||||
|
||||
- [ ] **Step 2: Szenario `fehler` deckt beide Mitarbeiter-Wege ab**
|
||||
|
||||
`useEmployees` fragt zuerst `GET https://portal.test/api/admin/employees` und fällt bei einem Fehler auf die Appwrite-Sammlung `employees` zurück.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fehler" "$W/backend.mjs"`
|
||||
Expected: In `fehler` antworten beide Wege mit 500. Fehlt einer, wird er in `backend.mjs` ergänzt. Sonst lädt die Liste trotz Szenario, und Review Focus 3 prüft nichts.
|
||||
|
||||
- [ ] **Step 3: Ansichten in `views.mjs` ergänzen**
|
||||
|
||||
Die Schritte haben die Signatur `steps(page, ctx)` aus der Übersicht. Alle Ansichten setzen zuerst die Uhr (`page.clock.setFixedTime`, ohne die Zeit einzufrieren) und klicken dann „Heute“. Das ist nötig, weil die Seite die aktuelle Woche beim ersten Rendern bestimmt. Eine Zusicherung, die nicht stimmt, wirft einen Fehler (Verstoß `step-failed`, in jedem Gate aktiv).
|
||||
|
||||
| ID | Daten | Schritte | Zusicherung |
|
||||
|---|---|---|---|
|
||||
| `planboard` | `planboard` | Uhr `2026-10-07T10:00:00+02:00`, „Heute“ | Kopf `Kalenderwoche 41 · 5. Oktober bis 11. Oktober · 13 Termine`. 7 `.day`, genau 2 `.day.weekend`, `.day.today .dn` = „07.10“, 13 `a.appt`, 2 `a.appt.akquise`, 1 Mal „Keine Termine“ (Sonntag). Je `.day` gilt `scrollWidth <= clientWidth`. Der Donnerstag hat 6 `a.appt`, jeder beginnt unter dem Ende des vorigen |
|
||||
| `planboard-woche-vor` | `planboard` | wie oben, dann „Nächste Woche“ | Kopf `Kalenderwoche 42 · 12. Oktober bis 18. Oktober · 1 Termin` |
|
||||
| `planboard-leer` | `planboard` | wie oben, zweimal „Nächste Woche“ | Kopf `Kalenderwoche 43 · 19. Oktober bis 25. Oktober · 0 Termine`, 7 Mal „Keine Termine“ |
|
||||
| `planboard-altdaten` | `planboard` | Uhr `2026-05-04T10:00:00+02:00`, „Heute“, „Vorherige Woche“ | Kopf `Kalenderwoche 18 · 27. April bis 3. Mai · 2 Termine`. Der Text enthält weder `undefined` noch `null` noch `NaN`. Der Freitagstermin hat einen `href`, der auf `/tickets/ohne-woid-alt` endet, und sein Text enthält kein „#“ |
|
||||
| `planboard-fehler` | `planboard`, Szenario `fehler` | wie `planboard` | 13 `a.appt`. Der Montagstermin zeigt „Kenso Grimm“ statt „KNSO“, der Samstagstermin „Ehemalige Kollegin“. `ctx.backend.calls` enthält je einen GET auf `…/api/admin/employees` und auf `…/collections/employees/documents` (beide Wege wurden versucht) |
|
||||
| `planboard-tastatur` | `planboard` | wie `planboard`. Tab drücken (höchstens 40 Mal), bis `document.activeElement` ein `a.appt` ist | Die Outline des fokussierten Elements ist `solid` mit 2 px. Enter führt zu `/tickets/100201`. Danach „Zurück“ und Warten auf `.week` |
|
||||
|
||||
- [ ] **Step 4: Gate `p3` in `gates.mjs` anlegen**
|
||||
|
||||
`p3` ist `p2` plus die Ansichten dieses Pakets im bereichsbezogenen Gate `icon-svg` (Schema der Übersicht). `step-failed`, `external-request` und `console-error` sind ohnehin aktiv, `ignore` von `p2` bleibt erhalten:
|
||||
|
||||
```js
|
||||
GATES.p3 = { ...GATES.p2, scoped: { ...GATES.p2.scoped, 'icon-svg': { ...GATES.p2.scoped['icon-svg'], views: [...GATES.p2.scoped['icon-svg'].views, 'planboard', 'planboard-*'] } } }
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Vorher-Lauf (muss scheitern)**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3-vorher && "$W/shots.sh" p3-vorher "$W/shots/p3-vorher" --only planboard --gate p3`
|
||||
Expected: Exit 1, `icon-svg` findet die zwei Chevron-Icons (dazu `step-failed`, weil die neue Kopfzeile fehlt). Die Bilder `shots/p3-vorher/1440/planboard.png` und `390/planboard.png` sind die Vorher-Bilder mit den neuen Testdaten. Die anderen fünf Ansichten brechen vor dem Umbau an den fehlenden Knöpfen und werden hier nicht gestartet.
|
||||
|
||||
### Task 5: Planboard-CSS und Seite
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css` (neuer Block „Planboard (P3)“ unmittelbar vor `/* ===== KOMPAT (bis P10) ===== */`)
|
||||
- Modify: `src/pages/PlanboardPage.jsx` (Zeilen 1–119 ersetzen)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes:
|
||||
- `PageHeader({ title, subtitle, actions })`, `Button({ variant })` aus P1.
|
||||
- `ticketPath(ref)` und `ticketRef(ticket)` aus P2.
|
||||
- `planboardView.js` aus Task 3.
|
||||
- `groupAppointmentsByDay`, `countInRange`, `dayKey` aus `lib/planboard.js`.
|
||||
- Tokens `--bg-hover`, `--bg-subtle`, `--appt-hover`, `--appt-akquise`, `--appt-akquise-hover`, `--accent`.
|
||||
- Produces: die Klassen `.week`, `.day` (`.today`, `.weekend`), `.day-head` (`.wd`, `.dn`), `.appt` (`.akquise`, `.tm`, `.tt`, `.cu`), `.none`. Spätere Pakete benutzen sie nicht.
|
||||
|
||||
- [ ] **Step 1: CSS-Block anlegen**
|
||||
|
||||
Alle Werte aus `quelle/styles.css`, Zeilen 379–397, mit den Tokens statt der Literale `#eef1f4`, `#fdf4e3` und `#fbeccd`. Zusätzlich gelten diese Ergänzungen:
|
||||
|
||||
| Selektor | Wert |
|
||||
|---|---|
|
||||
| `.day-head` | zusätzlich `flex-wrap: wrap; row-gap: 0` |
|
||||
| `.appt` | zusätzlich `overflow-wrap: anywhere` |
|
||||
| `.day.weekend .appt:not(.akquise)` | `background: var(--bg)` |
|
||||
| `.day.weekend .appt:not(.akquise):hover` | `background: var(--appt-hover)` |
|
||||
| `@media (max-width: 860px)` | am Ende des Blocks: `.week { grid-template-columns: 1fr; border-top: 0; min-height: 0 }`, `.day { border-left: 0; padding: 12px 0; border-top: 1px solid var(--line) }`, `.day.weekend { background: none }`, `.day.weekend .appt:not(.akquise) { background: var(--bg-hover) }` (Quelle: Zeilen 535–537) |
|
||||
|
||||
Der Block beginnt mit `/* ===== Planboard (P3) ===== */`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && sed -n '/Planboard (P3)/,/KOMPAT (bis P10)/p' src/styles/global.css | rg -n "!important|#[0-9a-fA-F]{3,8}\b"`
|
||||
Expected: nur ein Treffer, `#fff` an `.day.today .dn`. Gab es in Task 1 Step 4 Mobil-Regeln aus P1 für `.week` oder `.day`, sind sie jetzt aus dem P1-Block entfernt, sodass sie nur noch hier stehen (`rg -n "^\s*\.(week|day)\b" src/styles/global.css` zeigt nur Zeilen im P3-Block).
|
||||
|
||||
- [ ] **Step 2: `PlanboardPage.jsx` neu schreiben**
|
||||
|
||||
Der Code der Seite (State, `useWorkorders({ limit: 500 })`, `useEmployees`, `byDay`, `weekCount`, `navigateWeek`, `goToday`) bleibt. Das Markup wird:
|
||||
|
||||
| Element | Umsetzung |
|
||||
|---|---|
|
||||
| Wurzel | `<div className="page">` |
|
||||
| Kopf | `<PageHeader title="Planboard" subtitle={planboardSubtitle({ weekStart: currentWeek, count: weekCount, loading })} actions={…} />` |
|
||||
| Aktionen | drei Knöpfe in dieser Reihenfolge: `<Button variant="quiet" aria-label="Vorherige Woche">←</Button>`, `<Button>Heute</Button>`, `<Button variant="quiet" aria-label="Nächste Woche">→</Button>`. Kein schwarzer Knopf im Kopf |
|
||||
| Woche | `<div className="week">` mit sieben `<div className="day">`. Klassen `today` bei `isSameDay(day, new Date())` und `weekend` ab dem sechsten Tag. Attribute `role="group"`, `aria-label={labels.full}`, `aria-current="date"` nur bei heute |
|
||||
| Tageskopf | `<div className="day-head"><span className="wd">{labels.weekday}</span><span className="dn">{labels.number}</span></div>` |
|
||||
| Leerer Tag | `<div className="none">Keine Termine</div>` |
|
||||
| Termin | `<Link className={appointmentClass(entry.type)} to={ticketPath(ticketRef({ woid: entry.woid, $id: entry.id }))} title={appointmentTitle(entry)}>` mit `.tm` (`appointmentTimeLabel`), `.tt` (`entry.customerName \|\| entry.topic`), einem `.cu` mit `entry.topic` (nur bei vorhandenem Kundennamen) und einem `.cu` mit `appointmentOwnerLabel(entry)` |
|
||||
|
||||
Es entfallen: alle Icon-Importe, `Card`, `useNavigate`, `TYPE_COLORS`, `format` für die Kopfzeile und jedes `style`.
|
||||
|
||||
- [ ] **Step 3: Hygiene prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|navigate\(|Card|TYPE_COLORS" src/pages/PlanboardPage.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 4: Bauen und Gate**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3 && "$W/shots.sh" p3 "$W/shots/p3" --gate p3`
|
||||
Expected:
|
||||
- Exit 0, alle Zusicherungen aus Task 4 Step 3 erfüllt, `icon-svg` ist für die `planboard*`-Ansichten sauber.
|
||||
- In `shots/p3/390x844/planboard.png` stehen die Tage untereinander, der Mittwoch trägt die schwarze Zahl, und es gibt keine Spaltenlinien.
|
||||
- In `shots/p3/1440x900/planboard.png` sind Samstag und Sonntag hell abgesetzt, die Donnerstagsspalte ist länger, und die Akquise-Termine sind hell orange.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/PlanboardPage.jsx src/styles/global.css
|
||||
git commit -m "Planboard: Wochenansicht ohne Karten (Tagesspalten, heute als Pille, Termine als Links)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Gate P3, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0, `# fail 0`, `# pass` = N0 + 9. Liegen neue Commits auf `test`, kann die Zahl höher sein; sie wird notiert.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p3 && "$W/shots.sh" p3 "$W/shots/p3" --gate p3`
|
||||
Expected:
|
||||
- Gate `p3` Exit 0.
|
||||
- Keine Verstöße gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request` und `step-failed`, auch auf allen übrigen Ansichten.
|
||||
- Die Planboard-Ansichten haben kein `icon-svg`.
|
||||
|
||||
- [ ] **Step 3: Bundle-Größe**
|
||||
|
||||
Die Größen stehen in der Ausgabe von `check.sh` (Step 1). Sie werden mit dem Stand von `design-p2` verglichen. Das JS wird kleiner (zwei Icon-Importe weniger), das CSS wächst um rund 1 KB.
|
||||
|
||||
- [ ] **Step 4: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p2..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P3. Schwerpunkte:
|
||||
- keine Icons, keine `style={{`, kein Farbliteral in der Seite
|
||||
- `ticketPath(ticketRef(…))` ist das einzige Linkziel
|
||||
- das `aria-label` der Spalten und der Pfeilknöpfe
|
||||
- der CSS-Block benutzt nur Tokens, kein `!important`
|
||||
- `lib/planboard.js` ist unverändert (`git diff design-p2 -- src/lib/planboard.js` ist leer)
|
||||
|
||||
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only planboard,planboard-fehler`.
|
||||
|
||||
- [ ] **Step 5: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p3 && git log --oneline origin/test..design-p3 && git rev-parse design-p3`
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- die Screenshots aus `shots/p3/1440x900` und `shots/p3/390x844` (Auswahl: `planboard`, `planboard-woche-vor`, `planboard-fehler`) und den Vorher-Vergleich aus `shots/p3-vorher`
|
||||
- den Hinweis, dass der Tageskopf die heutigen Texte behält (der Prototyp kürzt auf „Mo 5.“) und der Prototyp-Fußtext samt Legende fehlt
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p3^{commit}:refs/heads/test'`
|
||||
- die Rücknahme: `git revert --no-commit design-p2..design-p3`, Commit und Push durch den Eigentümer (gilt nur, solange P3 das jüngste Paket ist)
|
||||
|
||||
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers: Planboard öffnen, Woche vor und zurück, ein Termin öffnet das Ticket.
|
||||
@@ -0,0 +1,618 @@
|
||||
# WOMS „Weißes Design“ P4: Kunden – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P4 sieht der Kundenbereich wie im Prototyp aus:
|
||||
- Kundenliste als Tabelle mit den Reitern Feste Kunden · Leads · Archiviert · Alle samt Zählern, mit Suche und Abzeichen
|
||||
- „Neuer Kunde“ und „Neuer Lead“ als Dialoge, auch im Zuordnen-Assistenten
|
||||
- Kundenseite mit Brotkrumen, Eyebrow und allen neun Reitern. Übersicht als zwei Spalten mit Schlüsselwerten, Stammdaten weiter direkt bearbeitbar, Tickets-Reiter mit Links
|
||||
- Funktionen, Protokoll und Abo-Status ohne Karten und ohne Icons, neuer Grundstil in `admin-areas.css`
|
||||
|
||||
**Architecture:**
|
||||
- Filter, Zähler und Abzeichen sind reine Funktionen in der neuen `src/lib/customerList.js` mit Tests. `lib/leads.js` importiert Appwrite und ist unter Node nicht ladbar. Deshalb ziehen `customerStage` und `isArchived` dorthin um, und `leads.js` re-exportiert sie. Alle bestehenden Importe bleiben gültig.
|
||||
- Tabellenzeilen haben als erste Zelle einen echten Link, der Zeilenklick läuft über `isRowActivation` aus `src/lib/rowClick.js` (entsteht in P2, P4 baut den Helfer nicht neu).
|
||||
- Die beiden Anlege-Formulare rendern `<Dialog>` aus P1.
|
||||
- CSS: ein kleiner Block „Kunden (P4)“ in `global.css`, dazu der neue Grundstil in `src/styles/admin-areas.css`.
|
||||
|
||||
**Tech Stack:** React 18, react-router-dom 6, reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Seitenkopf, Tabelle, Reiter, Badge, Formular, Dialog, Meldungen, Laden), „Ticket als eigene Seite / Route und Links“, „Texte“, „Risiken“ (Zeile zu `feature/terminplaner-tp2`) und Paket P4. Detail im Anhang `2026-10-04-weisses-design-umbaukarte.md`, Abschnitte 5 (P4), 3, 4 und 7.5. Globale Regeln und gemeinsame Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, aktuelles Verzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"` (Regeln der Übersicht).
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P4 kommt hinzu:
|
||||
|
||||
- Zeilenangaben gelten für den Stand `603d08c`. Nach P1 bis P3 können sie leicht verschoben sein, maßgeblich sind die genannten Namen.
|
||||
- Voraussetzung sind das Tag `design-p3` und aus P2 `ticketPath` und `ticketRef` (`lib/ticketRoutes.js`), `ticketNumber` (`lib/ticketView.js`), `isRowActivation` (`lib/rowClick.js`) und die Klasse `.row-link`.
|
||||
- Geändert werden nur `CustomersPage.jsx`, `CustomerDetailPage.jsx`, `components/customer/*`, `admin-areas.css`, ein Block in `global.css` (dazu fällt dort die KOMPAT-Regel `.side-logo`), in `lib/leads.js` zwei Funktionen (Umzug) und eine Stelle in `AssignProjectPage.jsx` (Aufruf von `CreateCustomerForm`). Neu sind `lib/customerList.js` und sein Test.
|
||||
- Unverändert bleiben `lib/customerTabs.js` (der Test pinnt `[id, label]`, Zähler kommen beim Rendern), `lib/funktionenView.js`, `useCustomers`, `customerAdminApi`, `teamApi`, `CopyableCredential` und `AssignedProjectCard` (P5), Chat, Gutschriften, Angebote (P6) und `CustomerInvoices` (P7).
|
||||
- **tp2 zuerst klären:** `feature/terminplaner-tp2` hat 5 Commits (`6f03712`, `c001554`, `857e001`, `45e5dc3`, `225e2f1`) vor `test`. Sie ändern `CustomerFunktionen.jsx`, `lib/funktionenView.js`, `admin-areas.css` und Rechtliches. P4 baut `CustomerFunktionen.jsx` und `admin-areas.css` neu. Task 1 stoppt deshalb und fragt den Eigentümer. Einzelne Commits werden nicht per Cherry-Pick geholt.
|
||||
- Texte bleiben wörtlich, auch die ae/ue-Schreibweisen („Strasse + Nr.“, „Fuer einen Lead …“). Neu sind nur Spaltenköpfe der neuen Tabellen, das Abzeichen „Wichtig“, „Wird geladen …“, „Speichert …“, „Kopieren“ und „Kopiert“.
|
||||
- **Erreichbar bleiben** alle heutigen Bedienelemente: Reiter und Suche der Liste, Zeile öffnet die Kundenseite, „Neuer Kunde“ (Admin) und „Neuer Lead“ mit beiden Formularen, auch im Zuordnen-Assistenten. Auf der Kundenseite „Kunde löschen“ (Admin, mit `window.confirm`), „Archivieren“/„Wiederherstellen“, „Kundenansicht öffnen“, der Weg zurück über die Brotkrume, die neun Reiter, „Speichern“, „Zum festen Kunden machen“, „Passwort ändern“/„Zugang anlegen“, Aktivieren und Abschalten mit Notiz, Site-Key kopieren und „Erneut versuchen“. Es gibt keine `data-testid` in diesen Dateien.
|
||||
- **Entscheidung (Reiter der Liste):** Reihenfolge Feste Kunden · Leads · Archiviert · Alle, Start bei Feste Kunden, nicht in der URL (wie heute).
|
||||
- **Entscheidung (Zähler und Suche):** Die Zähler nutzen dieselbe Funktion wie die Liste und beachten deshalb den Suchtext. Die Zahl im aktiven Reiter ist immer die Zeilenzahl. Die Suche durchsucht Name, Code, Standort, E-Mail wie heute und zusätzlich den Ansprechpartner, weil die Tabelle ihn zeigt (der Prototyp tut es auch).
|
||||
- **Entscheidung (Spalten der Liste):** Unternehmen mit Ansprechpartner als `.cell-sub`, Kundennummer (`hide-sm`), Standort, E-Mail und Telefon (`hide-md`). Keine Spalte „Offene Tickets“ (Spec), keine Fußzeile.
|
||||
- **Entscheidung (Abzeichen):** Lead grau (`muted`), Abgesagt und Archiviert `plain`, Wichtig orange (`warn`, wie der frühere Stern). „Fester Kunde“ entfällt als Abzeichen, der Reiter sagt es, die Eyebrow der Kundenseite nennt es.
|
||||
- **Entscheidung (erste Zelle):** `<div className="cell-title">` mit `<Link className="row-link">` um den Namen und den Abzeichen dahinter, wie in der Ticketliste aus P2.
|
||||
- **Entscheidung (Kundenseite):** Eyebrow „Kundennummer · Stufe“ (Stufe als Klartext), h1 nur der Name (Rückfall E-Mail, dann „–“). „Wichtig“ und „Archiviert“ stehen als Abzeichen in der `.sub`-Zeile vor Standort · E-Mail · Telefon. Kopfknöpfe: „Kunde löschen“ (Admin, roter Textlink), „Archivieren“/„Wiederherstellen“, „Kundenansicht öffnen“, keiner schwarz. „Alle Kunden“ wird die Brotkrume „Kunden“. Die Reiter bekommen keine Zähler (zusätzliche Abfragen, die Spec nennt Zähler nur für die Liste).
|
||||
- **Entscheidung (Übersicht):** Schwarz ist nur „Speichern“. „Zum festen Kunden machen“ und „Passwort ändern“/„Zugang anlegen“ sind Standardknöpfe. Rückmeldungen laufen über `.msg` (`ok`, `err`, `warn`).
|
||||
- **Entscheidung (Tickets-Reiter):** Spalten „Ticket“ (Nummer, Betreff, „Dringend“, darunter die Art roh wie heute) und „Status“, keine zusätzlichen Abfragen.
|
||||
- **Entscheidung (Dialoge):** `CreateLeadForm` im Standarddialog mit einer `.field-row`, `CreateCustomerForm` `wide` mit zwei `.field-row-3`. Fuß wie im Prototyp: `<span className="grow" />`, Abbrechen (Standardknopf mit `type="button"`, sonst löst er in `<Dialog as="form">` das Absenden aus), Absenden (schwarz, `type="submit"`). Abbrechen ist wie × und Esc während `busy` gesperrt. Beide Formulare bleiben in `CustomersPage.jsx`, der Export von `CreateCustomerForm` bleibt.
|
||||
- **Entscheidung (Funktionen und Protokoll):** Funktionen: eine Tabelle mit einer Projektzeile über den Funktionszeilen, die Bestätigung als eigene Zeile darunter. Dieselbe Zusatzzeile kann später das Panel von tp2 aufnehmen. Protokoll: Tabelle „Zeit“, „Mitarbeiter“, „Aktion“ mit dem Zusatztext als `.cell-sub`.
|
||||
- **Bekannt und unverändert:** `CustomerDetailPage.load()` ersetzt die Seite beim Nachladen durch den Ladezustand, die Meldung „Gespeichert“ bleibt deshalb nicht stehen. Das ist heute schon so und kein P4-Thema.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Alle fünf Punkte der Übersicht treffen P4. Die Ansichten stehen in Task 3.
|
||||
|
||||
1. **Altdaten** (`kunden-altdaten*`, `kunde-altdaten`): Kunden ohne Kundennummer, Standort, E-Mail, Telefon und Ansprechpartner zeigen „–“. Ein Kunde ohne Namen zeigt die E-Mail, ohne beides „–“. Im Text steht nie `undefined`, `null` oder `NaN`.
|
||||
2. **Lange Inhalte:** Firmenname mit 60 Zeichen, ein Kunde mit den Abzeichen Wichtig, Lead und Archiviert, Funktionen mit langem Projektnamen, langer URL, Site-Key und Notiz. Kein waagerechtes Scrollen bei 390 px.
|
||||
3. **Teilausfall** (`fehler-kunde*`): Antworten die Portal-Wege `portal`, `abo`, `protokoll` und `funktionen` mit 500, zeigen Protokoll, Abo und Funktionen `.msg.err`, die Übersicht „nicht abrufbar“. Die Reiter bleiben bedienbar.
|
||||
4. **Nur Tastatur** (`kunden-tastatur`, `dlg-*`): Ein Zeilenlink ist per Tab erreichbar, Enter öffnet die Kundenseite. Esc schließt die Dialoge, der erste Tab im offenen Dialog bleibt im Dialog. Ein Klick in die Zeile neben dem Link öffnet genau einmal.
|
||||
5. **Viele Einträge** (`kunden-viel`): 300 zusätzliche Kunden, wie heute ohne Seitenwechsel. Die Zähler stimmen in jedem Reiter mit den Zeilen überein.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Paketstart und tp2-Prüfung
|
||||
|
||||
P4 baut `CustomerFunktionen.jsx` und `admin-areas.css` neu, beide Dateien ändert auch `feature/terminplaner-tp2`. Vor dem ersten Umbau wird deshalb geklärt, in welcher Reihenfolge beide Arbeiten live gehen.
|
||||
|
||||
**Files:** keine Änderung (nur Lesen und Prüfen).
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tag `design-p3` und die P2-Bausteine `ticketPath`, `ticketRef`, `ticketNumber`, `isRowActivation` und `.row-link`.
|
||||
- Produces: die Ausgangs-Testzahl N3 und die Entscheidung des Eigentümers zu tp2.
|
||||
|
||||
- [ ] **Step 1: Paketstart**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p3 HEAD && git rebase origin/test && "$W/check.sh"`
|
||||
Expected:
|
||||
- Alle Befehle enden mit Exit 0, `# fail 0`. Schlägt `merge-base` fehl, ist `design-p3` nicht im Branch: Stopp und melden.
|
||||
- Die Zahl `# pass` ist N3. Soll nach P4: N3 + 6.
|
||||
|
||||
- [ ] **Step 2: Parallele Arbeit an `feature/terminplaner-tp2`**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/CustomersPage.jsx src/pages/CustomerDetailPage.jsx src/components/customer src/styles/admin-areas.css src/styles/global.css src/lib/leads.js src/lib/funktionenView.js src/pages/AssignProjectPage.jsx`
|
||||
Expected: keine Ausgabe. Gibt der Befehl Zeilen aus (Stand 2026-10-05: `6f03712`, `c001554`, `857e001`, `45e5dc3`), **stoppt der Agent und fragt den Eigentümer**:
|
||||
- Er empfiehlt, tp2 zuerst nach `test` zu bringen.
|
||||
- Er holt keinen Commit einzeln per Cherry-Pick. `6f03712` allein ließe `CustomerFunktionen.jsx` und `lib/funktionenView.js` in einem Zwischenstand, den die übrigen tp2-Commits wieder umbauen.
|
||||
|
||||
Weiter geht es erst nach der Antwort:
|
||||
- **Fall A, tp2 ist in `test`:** Der Rebase aus Step 1 wird wiederholt, dieser Step ist dann leer.
|
||||
- **Fall B, der Eigentümer legt P4 vor tp2:** Er nennt, wer die Konflikte in `CustomerFunktionen.jsx`, `lib/funktionenView.js` und `admin-areas.css` später auflöst. Die Übergabe (Task 8) hält das fest.
|
||||
|
||||
- [ ] **Step 3: P2-Bausteine sind da**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "export function (ticketPath|ticketRef)" src/lib/ticketRoutes.js && rg -n "export function ticketNumber" src/lib/ticketView.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -m 2 -n "\.row-link" src/styles/global.css`
|
||||
Expected: Treffer in allen vier Dateien. Sonst Stopp: P4 darf erst nach P2 laufen.
|
||||
|
||||
- [ ] **Step 4: Stand von `CustomerFunktionen.jsx`**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -c "abgeschaltetText" src/lib/funktionenView.js; rg -c "AnleitungPanel|TextformPanel" src/components/customer/CustomerFunktionen.jsx`
|
||||
Expected:
|
||||
- Fall A (tp2 ist in `test`): Treffer in beiden Dateien. Task 6 enthält dann die Zeile „tp2-Zusatz“.
|
||||
- Fall B (der Eigentümer hat P4 vor tp2 freigegeben): keine Treffer. In Task 6 entfallen die Zeile „tp2-Zusatz“ und `abgeschaltetText`, und in Task 3 entfällt die Zusicherung zum Abschaltgrund in `kunde-funktionen`.
|
||||
|
||||
### Task 2: `customerList.js` (Filter, Zähler, Abzeichen)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/customerList.js`
|
||||
- Modify: `src/lib/leads.js` (Zeilen 8–18: `customerStage` und `isArchived` samt Kommentar ersetzen)
|
||||
- Test: `tests/unit/customerList.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nichts (reine Datei ohne Importe).
|
||||
- Produces, alle aus `src/lib/customerList.js`, die ersten vier auch aus `src/lib/leads.js`:
|
||||
- `customerStage(c) → 'lead' | 'lost' | 'customer'` und `isArchived(c) → boolean`, unverändert verschoben
|
||||
- `filterCustomers(customers, { stage = 'customer', search = '' } = {}) → Kunde[]`
|
||||
- `stageCounts(customers, search = '') → { customer, lead, archived, all }`
|
||||
- `CUSTOMER_TABS: [{ id, label }]`
|
||||
- `customerBadges(c, { stage = true } = {}) → [{ key, label, tone }]`
|
||||
- `stageLabel(stage)`, `customerEyebrow(c)`, `customerTitle(c)` → string
|
||||
|
||||
- [ ] **Step 1: Failing Tests schreiben**
|
||||
|
||||
```js
|
||||
const { CUSTOMER_TABS, customerStage, isArchived, filterCustomers, stageCounts, customerBadges, stageLabel, customerEyebrow, customerTitle } =
|
||||
await loadModule('src/lib/customerList.js')
|
||||
|
||||
const A = { $id: 'a', name: 'Alpha GmbH', code: 'K-1', location: 'Kaiserslautern', email: 'info@alpha.de', contactName: 'Anna Alt', customerStatus: 'customer' }
|
||||
const B = { $id: 'b', name: 'Beta', customerStatus: 'lead' }
|
||||
const C = { $id: 'c', name: 'Gamma', customerStatus: 'lost' }
|
||||
const D = { $id: 'd', name: 'Delta' } // Bestandsdaten ohne Status gelten als fester Kunde
|
||||
const E = { $id: 'e', name: 'Epsilon', customerStatus: 'lead', archived: true, importantCustomer: true }
|
||||
const F = { $id: 'f', name: 'Zeta', customerStatus: 'customer', archived: true }
|
||||
const ALL = [A, B, C, D, E, F]
|
||||
const ids = (list) => list.map((c) => c.$id)
|
||||
|
||||
test('CUSTOMER_TABS, customerStage, isArchived', () => {
|
||||
assert.deepEqual(CUSTOMER_TABS, [
|
||||
{ id: 'customer', label: 'Feste Kunden' }, { id: 'lead', label: 'Leads' },
|
||||
{ id: 'archived', label: 'Archiviert' }, { id: 'all', label: 'Alle' },
|
||||
])
|
||||
assert.equal(customerStage({ customerStatus: 'LEAD' }), 'lead')
|
||||
assert.equal(customerStage({ customerStatus: 'Lost' }), 'lost')
|
||||
for (const c of [{}, { customerStatus: '' }, { customerStatus: 'sonstwas' }, null, undefined]) assert.equal(customerStage(c), 'customer')
|
||||
assert.equal(isArchived({ archived: true }), true)
|
||||
for (const c of [{}, { archived: false }, null, undefined]) assert.equal(isArchived(c), false)
|
||||
})
|
||||
|
||||
test('filterCustomers: jeder Reiter', () => {
|
||||
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'customer' })), ['a', 'd'])
|
||||
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'lead' })), ['b'])
|
||||
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'archived' })), ['e', 'f'])
|
||||
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'all' })), ['a', 'b', 'c', 'd'])
|
||||
assert.deepEqual(ids(filterCustomers(ALL)), ['a', 'd'])
|
||||
assert.deepEqual(ids(filterCustomers(ALL, { stage: 'x' })), [])
|
||||
assert.deepEqual(filterCustomers(undefined, { stage: 'all' }), [])
|
||||
})
|
||||
|
||||
test('filterCustomers: Suche in Name, Code, Standort, E-Mail und Ansprechpartner', () => {
|
||||
const find = (search, stage = 'all') => ids(filterCustomers(ALL, { stage, search }))
|
||||
for (const s of ['ALPHA', 'k-1', 'kaisers', 'info@alpha', 'anna']) assert.deepEqual(find(s), ['a'], s)
|
||||
assert.deepEqual(find('xyz'), [])
|
||||
assert.deepEqual(find('eta', 'lead'), ['b'])
|
||||
assert.deepEqual(find(''), ['a', 'b', 'c', 'd'])
|
||||
assert.deepEqual(ids(filterCustomers([{ $id: 'n' }], { stage: 'all', search: 'undefined' })), [])
|
||||
})
|
||||
|
||||
test('stageCounts: dieselbe Filterfunktion wie die Liste', () => {
|
||||
assert.deepEqual(stageCounts(ALL), { customer: 2, lead: 1, archived: 2, all: 4 })
|
||||
assert.deepEqual(stageCounts(ALL, 'eta'), { customer: 0, lead: 1, archived: 1, all: 1 })
|
||||
assert.deepEqual(stageCounts([]), { customer: 0, lead: 0, archived: 0, all: 0 })
|
||||
})
|
||||
|
||||
test('customerBadges: Reihenfolge, Töne, Option stage', () => {
|
||||
assert.deepEqual(customerBadges(E), [
|
||||
{ key: 'important', label: 'Wichtig', tone: 'warn' },
|
||||
{ key: 'lead', label: 'Lead', tone: 'muted' },
|
||||
{ key: 'archived', label: 'Archiviert', tone: 'plain' },
|
||||
])
|
||||
assert.deepEqual(customerBadges(C), [{ key: 'lost', label: 'Abgesagt', tone: 'plain' }])
|
||||
for (const c of [A, D, null]) assert.deepEqual(customerBadges(c), [])
|
||||
assert.deepEqual(customerBadges(E, { stage: false }).map((b) => b.key), ['important', 'archived'])
|
||||
})
|
||||
|
||||
test('stageLabel, customerEyebrow, customerTitle', () => {
|
||||
assert.deepEqual(['customer', 'lead', 'lost'].map(stageLabel), ['Fester Kunde', 'Lead', 'Abgesagt'])
|
||||
assert.equal(customerEyebrow(A), 'K-1 · Fester Kunde')
|
||||
assert.equal(customerEyebrow(B), 'Lead')
|
||||
assert.equal(customerEyebrow({ code: ' ', customerStatus: 'lost' }), 'Abgesagt')
|
||||
assert.equal(customerTitle(A), 'Alpha GmbH')
|
||||
assert.equal(customerTitle({ email: 'x@y.de' }), 'x@y.de')
|
||||
assert.equal(customerTitle({}), '–')
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/customerList.test.mjs`
|
||||
Expected: FAIL, `src/lib/customerList.js` fehlt.
|
||||
|
||||
- [ ] **Step 3: `customerList.js` implementieren**
|
||||
|
||||
- `customerStage` und `isArchived` werden Zeile für Zeile aus `leads.js` übernommen.
|
||||
- `filterCustomers`:
|
||||
- Ohne Liste `[]`, die Reihenfolge der Eingabe bleibt.
|
||||
- Der Reiter `archived` liefert nur Archivierte, alle anderen Reiter schließen sie aus.
|
||||
- Stufe `lost` erscheint nur im Reiter `all`. `customer` und `lead` liefern ihre Stufe, ein unbekannter Reiter `[]`.
|
||||
- Die Suche ist ein Teilstring-Vergleich in Kleinbuchstaben ohne Trim über `${name} ${code} ${location} ${email} ${contactName}`, jedes Feld mit `|| ''`. Eine leere Suche filtert nicht.
|
||||
- `stageCounts`: je Eintrag von `CUSTOMER_TABS` die Länge von `filterCustomers(customers, { stage, search })`.
|
||||
- `customerBadges`: Wichtig (`importantCustomer`, Ton `warn`), dann die Stufe (`lead` Ton `muted`, `lost` „Abgesagt“ Ton `plain`, `customer` nichts, nur mit `stage: true`), dann Archiviert (Ton `plain`).
|
||||
- `customerEyebrow` = `[String(c.code || '').trim(), stageLabel(customerStage(c))].filter(Boolean).join(' · ')`. `customerTitle` = `c.name || c.email || '–'`.
|
||||
|
||||
- [ ] **Step 4: `leads.js` umstellen**
|
||||
|
||||
In `src/lib/leads.js` werden die Definitionen von `customerStage` und `isArchived` durch `export { customerStage, isArchived, filterCustomers, stageCounts } from './customerList'` ersetzt. Alles andere bleibt, auch `isInvoiceReady`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "from '../lib/leads'" src`
|
||||
Expected: drei Importeure (`CustomersPage`, `CustomerDetailPage`, `AssignProjectPage`), alle ohne Änderung gültig.
|
||||
|
||||
- [ ] **Step 5: Test grün und Build**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/customerList.test.mjs && "$W/check.sh" --nur-build`
|
||||
Expected: PASS mit 6 Tests, Build Exit 0.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/customerList.js src/lib/leads.js tests/unit/customerList.test.mjs
|
||||
git commit -m "Kunden: Filter, Zaehler und Abzeichen als reine Funktionen (lib/customerList.js, Tests)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: Testdaten, Ansichten und Gate (Prüfung vor dem Umbau)
|
||||
|
||||
Alles liegt im Werkzeug außerhalb des Repos, deshalb gibt es keinen Commit.
|
||||
|
||||
**Files** (unter `$W/`):
|
||||
- Modify: `fixtures/standard.mjs`, `fixtures/altdaten.mjs`, `fixtures/viel.mjs`, `backend.mjs`, `views.mjs`, `gates.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createBackend`, `VIEWS`, `GATES` aus P0.
|
||||
- Produces: die Ansichten aus Step 3 und das Gate `p4`.
|
||||
|
||||
- [ ] **Step 1: Kunden-Testdaten prüfen und ergänzen**
|
||||
|
||||
- `standard`: Der „Abgesagte“ trägt `customerStatus: 'lost'` (nicht `crmStatus`). Soll-Zähler: Feste Kunden 6 · Leads 2 · Archiviert 1 · Alle 9.
|
||||
- `altdaten`: zusätzlich zu den Standardkunden fünf Kunden:
|
||||
1. `Müller-Lüdenscheidt Metallbau- und Anlagentechnik GmbH & Co.` (60 Zeichen), fest, `importantCustomer: true`, ohne Kundennummer, Standort, E-Mail, Telefon, Ansprechpartner
|
||||
2. Lead `Frisch Gelead UG` mit `importantCustomer: true`
|
||||
3. archivierter Lead `Archivierter Wichtiger Lead` mit `importantCustomer: true`
|
||||
4. fester Kunde ohne Namen mit E-Mail `ohne-name@example.test`
|
||||
5. fester Kunde mit leerem Namen und ohne E-Mail
|
||||
- `viel`: zusätzlich 300 Kunden `Testkunde 001` bis `Testkunde 300`, 200 fest und 100 Leads. Soll-Zähler: 206 · 102 · 1 · 309.
|
||||
|
||||
- [ ] **Step 2: Portal-Antworten ergänzen**
|
||||
|
||||
Falls P0 sie nicht liefert (unter `https://portal.test/api/team`):
|
||||
|
||||
| Pfad | Antwort |
|
||||
|---|---|
|
||||
| `/customers/:id/portal` | `{ hasAccess: true, lastLoginAt: '2026-10-04T08:15:00.000Z' }` |
|
||||
| `/customers/:id/abo` | ein aktives Abo mit Zusatzspeicher, für einen zweiten Kunden `{ status: '' }` („kein Abo“) |
|
||||
| `/customers/:id/protokoll` | `{ entries }` mit 5 Einträgen: `geoeffnet`, `news_angelegt` mit Zusatztext, `termin_aktion` mit 120 Zeichen Zusatztext, einer ohne `staffName`, einer mit unbekannter Aktion `xyz` |
|
||||
| `/funktionen/:id` | `{ projects }` mit den drei Projekten unten |
|
||||
|
||||
Projekte der Funktionen:
|
||||
1. `Bäckerei Sonnenschein Website` mit `liveUrl`. News `active` mit `ticketWoid: '100131'`, Terminplaner `setup` mit `ticketWoid: '100140'` und `adminNote` von 80 Zeichen.
|
||||
2. Projekt mit 70 Zeichen langem Namen und `liveUrl` von 80 Zeichen ohne Leerzeichen. News `off`, Terminplaner `active` mit `siteKey: 'q74ehq7hvnpiymqy'` und `ticketMissing: true`.
|
||||
3. `Altes Projekt` ohne `liveUrl`. News `disabled` mit `disabledReason: 'abo_ende'` und `loeschungAm: '2026-11-04'`, Terminplaner `disabled` mit `disabledReason: 'abo_ende_vor_einrichtung'` und `loeschungAm: '2026-11-04'`.
|
||||
|
||||
Im Szenario `fehler` antworten `/customers/:id/portal`, `/abo`, `/protokoll` und `/funktionen/:id` mit 500.
|
||||
|
||||
- [ ] **Step 3: Ansichten in `views.mjs` ergänzen**
|
||||
|
||||
Die Schritte haben die Signatur `steps(page, ctx)` aus der Übersicht. Eine nicht erfüllte Zusicherung wirft einen Fehler (Verstoß `step-failed`, in jedem Gate aktiv). `zaehlerStimmen` klickt jeden der vier Reiter und vergleicht die Zahl im Reiter mit der Zahl der Zeilen `tbody tr.row` (leer: 0). `keinNull` wirft, wenn der Seitentext `undefined`, `null` oder `NaN` enthält.
|
||||
|
||||
| ID | Daten | Schritte | Zusicherung |
|
||||
|---|---|---|---|
|
||||
| `kunden` | `standard` | wie P0 | Reiter 6 · 2 · 1 · 9, `zaehlerStimmen` |
|
||||
| `kunden-leads`, `kunden-archiv`, `kunden-alle` | `standard` | je der Reiter | 2 Zeilen mit „Lead“ · 1 Zeile mit „Archiviert“ · 9 Zeilen, davon eine mit „Abgesagt“ |
|
||||
| `kunden-suche` | `standard` | Suche „Weber“ | 1 Zeile, Reiter 1 · 0 · 0 · 1 |
|
||||
| `kunden-altdaten`, `kunden-altdaten-leads`, `kunden-altdaten-archiv` | `altdaten` | Reiter Alle, Leads, Archiviert | `keinNull`, der 60-Zeichen-Name steht vollständig im Link, eine Zeile mit „Wichtig“ und „Lead“, eine mit „Wichtig“, „Lead“ und „Archiviert“ |
|
||||
| `kunden-viel` | `viel` | wie P0 | Reiter 206 · 102 · 1 · 309, `zaehlerStimmen` |
|
||||
| `kunden-tastatur` | `standard` | Tab, bis ein `tbody tr a.row-link` Fokus hat, dann Enter | Fokusring 2 px `solid`. Pfad beginnt mit `/customers/`. Nach „Zurück“ erhöht ein Klick in die Zeile neben dem Link `history.length` um genau 1 |
|
||||
| `dlg-lead-neu`, `dlg-kunde-neu` | `standard` | Klick „Neuer Lead“ bzw. „Neuer Kunde“ | `role="dialog"`, Titel „Neuer Lead (potenzieller Kunde)“ bzw. „Neuer Kunde mit Portal-Zugang“. Ein Tab landet im Dialog (`document.activeElement.closest('.dialog')`). Esc schließt, der Fokus kehrt zum Knopf zurück. Danach erneut öffnen |
|
||||
| `dlg-kunde-neu-fehler` | `standard` | Name und E-Mail füllen, Passwort „kurz“, absenden | `.msg.err` mit „Das Portal-Passwort muss mindestens 8 Zeichen haben.“, Dialog bleibt offen |
|
||||
| `zuordnen-kunde-neu` | `standard` | `/projects/zuordnen/<id>`, erste Art, „Ja, für einen Kunden“, „Neuer Kunde“ | Dialog „Neuer Kunde mit Portal-Zugang“ über der Seite |
|
||||
| `kunde-overview`, `kunde-tickets`, `kunde-invoices`, `kunde-projects`, `kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`, `kunde-protokoll` | `standard` | die neun Ansichten aus P0 (Reiter-IDs aus `src/lib/customerTabs.js`, siehe Übersicht). Fehlende werden als `/customers/<id>?tab=<reiter>` ergänzt. Für dieselbe URL entsteht keine zweite Ansicht | keine eigene Zusicherung, außer bei `kunde-funktionen` (nächste Zeile) |
|
||||
| `kunde-funktionen` (zusätzliche Schritte) | `standard` | Tab „Funktionen“, „Abschalten“ in der News-Zeile des ersten Projekts, „Abbrechen“, „Abschalten“ erneut | Nach dem ersten Klick steht die Zeile „News abschalten? Die Website zeigt dann keine Beiträge mehr.“ mit Feld „Notiz (optional)“, „Ja“ und „Abbrechen“. „Abbrechen“ schließt sie, am Ende ist sie offen, damit das Bild sie zeigt. Der Text enthält „Abgeschaltet (Abo beendet), Löschung am 04.11.2026“ (nur Fall A, siehe Task 1 Step 4) |
|
||||
| `kunde-lead`, `kunde-archiv`, `kunde-altdaten` | `standard`, `standard`, `altdaten` | Kunde „Gärtnerei Grünblick“, „Blumen Lehmann“, Kunde Nr. 1 aus Step 1 | Eyebrow „Lead“ mit „Zum festen Kunden machen“ · „Archiviert“ mit „Wiederherstellen“ · `keinNull`, langer Name in h1 und Brotkrume |
|
||||
| `kunde-nicht-gefunden` | `standard` | `/customers/gibt-es-nicht` | „Kunde nicht gefunden“ und Knopf „Zurück“ |
|
||||
| `fehler-kunde`, `fehler-kunde-funktionen`, `fehler-kunde-protokoll`, `fehler-kunde-abo` | `standard`, `fehler` | Übersicht bzw. `?tab=funktionen`, `?tab=protokoll`, `?tab=abo` | „nicht abrufbar“ beim letzten Login bzw. je ein `.msg.err`. Ein Klick auf „Übersicht“ zeigt die Stammdaten |
|
||||
|
||||
- [ ] **Step 4: Gate `p4` in `gates.mjs` anlegen**
|
||||
|
||||
`p4` ist `p3` plus die P4-Ansichten im bereichsbezogenen Gate `icon-svg` (Schema der Übersicht). `step-failed`, `external-request` und `console-error` sind ohnehin aktiv. Das Gate deckt auch `gutschriften`, `angebote` und `speicher` ab, die der neue Grundstil in `admin-areas.css` trifft.
|
||||
|
||||
```js
|
||||
const I3 = GATES.p3.scoped['icon-svg']
|
||||
GATES.p4 = { ...GATES.p3, scoped: { ...GATES.p3.scoped, 'icon-svg': {
|
||||
views: [...I3.views, 'kunden', 'kunden-*', 'dlg-lead-neu', 'dlg-kunde-neu', 'dlg-kunde-neu-fehler', 'kunde-overview', 'kunde-lead', 'kunde-archiv', 'kunde-altdaten', 'kunde-tickets', 'kunde-funktionen', 'kunde-protokoll', 'kunde-nicht-gefunden', 'fehler-kunde', 'fehler-kunde-funktionen', 'fehler-kunde-protokoll'],
|
||||
ignore: [...(I3.ignore || []), '.credential'],
|
||||
} } }
|
||||
```
|
||||
|
||||
`.credential` (`CopyableCredential`) ist bis P5 ausgenommen. Nicht im `icon-svg`-Umfang sind `kunde-abo` und `fehler-kunde-abo` (Angebote, P6), `kunde-projects` (P5), `kunde-chat`, `kunde-invoices`, `kunde-gutschriften` und `zuordnen-kunde-neu` (fremde Bausteine). Deshalb stehen die `fehler-kunde-*`-Ansichten einzeln in der Liste. Ihre P4-Anteile prüft Task 8 über `rg` auf Icon-Importe.
|
||||
|
||||
- [ ] **Step 5: Vorher-Lauf (muss scheitern)**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4-vorher && "$W/shots.sh" p4-vorher "$W/shots/p4-vorher" --only kunden,kunde-overview,kunde-funktionen,kunde-protokoll --gate p4`
|
||||
Expected: Exit 1 wegen `icon-svg`. Die Bilder in `shots/p4-vorher/1440` und `390` sind die Vorher-Bilder mit den neuen Testdaten.
|
||||
|
||||
### Task 4: Kundenliste und Dialoge
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/CustomersPage.jsx` (Zeilen 1–208 ersetzen)
|
||||
- Modify: `src/pages/AssignProjectPage.jsx` (Zeilen 275–289: Aufruf von `CreateCustomerForm`)
|
||||
- Modify: `src/styles/global.css` (nur die KOMPAT-Regel `.side-logo` löschen)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageHeader`, `Tabs`, `Badge`, `Button`, `EmptyState`, `Field`, `Dialog` (P1). `CUSTOMER_TABS`, `filterCustomers`, `stageCounts`, `customerBadges`, `customerTitle` (Task 2). `isRowActivation` aus `src/lib/rowClick.js` (P2). `createLead`, `useCustomers`, `useAuth` unverändert.
|
||||
- Produces:
|
||||
- `export function CreateCustomerForm({ onCancel, onCreate })` rendert einen `wide` Dialog. Die Signatur bleibt, P5 braucht sie.
|
||||
- `CreateLeadForm({ onCancel, onDone })` (nicht exportiert) rendert einen Dialog.
|
||||
|
||||
- [ ] **Step 1: Seite neu aufbauen**
|
||||
|
||||
| Element | Umsetzung |
|
||||
|---|---|
|
||||
| Kopf | `PageHeader title="Kunden" subtitle="Potenzielle Kunden (Leads) und feste Kunden – eine Pflege"`. Aktionen: für Admins `Button variant="primary"` „Neuer Kunde“, für alle `Button` „Neuer Lead“. Beide setzen `showCreate` direkt, kein Umschalten mehr |
|
||||
| Reiter | `Tabs` mit `CUSTOMER_TABS` und `count = stageCounts(customers, search)[id]`, bis zum Laden ohne Zähler |
|
||||
| Suche | `<div className="toolbar">` mit `<input className="input search" type="search" placeholder="Kunde suchen …" aria-label="Kunde suchen">`, filtert sofort wie heute |
|
||||
| Laden, leer | `<div className="loading">Wird geladen …</div>`. Leer: `EmptyState title="Keine Einträge" hint="Keine Kunden in dieser Ansicht."` |
|
||||
| Tabelle | `.table`, `<tr className="row">` mit `onClick`, der bei `isRowActivation(e)` zu `/customers/<$id>` navigiert |
|
||||
| Dialoge | `showCreate === 'lead'` rendert `CreateLeadForm`, `'customer'` und `isAdmin` rendert `CreateCustomerForm`, mit denselben Handlern wie heute. Bei Erfolg schließt die Seite, setzt bei Kunden den Reiter auf Feste Kunden und lädt neu |
|
||||
|
||||
Spalten (Klasse gilt für Kopf und Zelle):
|
||||
|
||||
| Kopf | Klasse | Inhalt |
|
||||
|---|---|---|
|
||||
| Unternehmen | – | `<div className="cell-title">` mit `<Link className="row-link" to=…>{customerTitle(c)}</Link>` und dahinter den `Badge`s aus `customerBadges(c)`. Darunter `.cell-sub` mit `contactName`, nur wenn vorhanden |
|
||||
| Kundennummer | `hide-sm num` | `code`, sonst `<span className="faint">–</span>` |
|
||||
| Standort | – | `location`, sonst „–“ |
|
||||
| E-Mail | `hide-md muted` | `email`, sonst „–“ |
|
||||
| Telefon | `hide-md muted nowrap num` | `phone`, sonst „–“ |
|
||||
|
||||
Formulare (jedes Feld ist ein `Field` mit `htmlFor` und `id`, `Dialog` bekommt `onClose={onCancel}` und `busy`, Fehler wie in P2 als `.msg.err` oben im Körper):
|
||||
- `CreateLeadForm`: `as="form"`, Titel „Neuer Lead (potenzieller Kunde)“. Im Körper der Hinweis „Fuer einen Lead reichen Unternehmen und E-Mail. Es wird automatisch ein Akquise-Ticket erstellt.“ (`.muted .small`), dann `.field-row` mit „Unternehmen“ (`required`) und „E-Mail“ (`type="email"`). Ein Fehler steht als `.msg.err` mit `role="alert"` als erstes Kind des Körpers. Fuß: `<Button type="button">` „Abbrechen“ und `Button variant="primary" type="submit"` „Lead anlegen + Akquise-Ticket“ (bei `busy` „Speichert …“). „Unternehmen erforderlich“ bleibt.
|
||||
- `CreateCustomerForm`: `wide`, Titel „Neuer Kunde mit Portal-Zugang“, zwei `.field-row-3`: „Unternehmen / Name“, „E-Mail (Portal-Login)“, „Kundennummer“ und „Telefon“, „Standort“, „Portal-Passwort (mind. 8 Zeichen)“. `name`, `required`, `type` und `autoComplete="new-password"` bleiben, ebenso die Texte „Das Portal-Passwort muss mindestens 8 Zeichen haben.“ und „Kunde konnte nicht angelegt werden.“. Fuß: `<Button type="button">` „Abbrechen“ und `Button variant="primary" type="submit"` „Kunde anlegen“ (bei `busy` „Speichert …“).
|
||||
|
||||
- [ ] **Step 2: Aufruf im Zuordnen-Assistenten**
|
||||
|
||||
In `AssignProjectPage.jsx` entfällt nur der Umschlag `<div style={{ marginBottom: 12 }}>` um `CreateCustomerForm`, der Dialog liegt über der Seite. Props und Handler bleiben. Der Rest der Datei gehört zu P5.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff --stat src/pages/AssignProjectPage.jsx`
|
||||
Expected: nur wenige Zeilen geändert (der Umschlag).
|
||||
|
||||
- [ ] **Step 3: Hygiene prüfen, `.side-logo` löschen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|badge-|\bdot\b" src/pages/CustomersPage.jsx; rg -w side-logo src`
|
||||
Expected: Aus `CustomersPage.jsx` keine Treffer. `side-logo` steht nur noch als KOMPAT-Regel in `src/styles/global.css`, keine Datei benutzt es. Die Spec lässt die Regel bis P4 stehen, deshalb wird sie jetzt gelöscht (samt Hover- und Kindregeln).
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -w side-logo src`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 4: Bauen und prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t4" --only kunden,kunden-leads,kunden-archiv,kunden-alle,kunden-suche,kunden-altdaten,kunden-altdaten-leads,kunden-altdaten-archiv,kunden-viel,kunden-tastatur,dlg-lead-neu,dlg-kunde-neu,dlg-kunde-neu-fehler,zuordnen-kunde-neu --gate p4`
|
||||
Expected: Exit 0 und alle Zusicherungen aus Task 3. Die Bilder zeigen Haarlinien, schwarzes „Neuer Kunde“ und Standard-„Neuer Lead“. Bei 390 px bleiben Unternehmen und Standort.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/CustomersPage.jsx src/pages/AssignProjectPage.jsx src/styles/global.css
|
||||
git commit -m "Kundenliste als Tabelle mit Reitern und Zaehlern, Neuer Kunde und Neuer Lead als Dialoge" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 5: Kundenseite und Kundenansicht-Knopf
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/CustomerDetailPage.jsx` (Kopf Zeilen 56–124, `OverviewTab` 126–212, `PortalAccessCard` 214–298, `CustomerTickets` 300–344, `CustomerProjects` 346–370)
|
||||
- Modify: `src/components/customer/KundenansichtButton.jsx` (Zeilen 1–36)
|
||||
- Modify: `src/styles/global.css` (neuer Block „Kunden (P4)“ vor dem KOMPAT-Block)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageHeader` (`crumb`, `eyebrow`, `subtitle`), `Tabs`, `Card`, `Badge`, `Button`, `EmptyState`, `StatusPill`, `UrgentPill` (P1). `customerEyebrow`, `customerBadges`, `customerTitle`, `customerStage`, `isArchived` (Task 2). `ticketPath(ref)`, `ticketRef(ticket)` (P2), `ticketNumber(ticket)` (P2). `isRowActivation` aus `src/lib/rowClick.js` (P2).
|
||||
- Produces: `KundenansichtButton({ customer, variant = 'default', checkAccess = true })`. Die Voreinstellung wechselt von `primary` auf `default`. Die anderen Aufrufer (`CreditDetail`, P6) übergeben `ghost` und sehen gleich aus. Sonst nichts Neues, die Aufrufe der Reiter-Inhalte bleiben wie heute.
|
||||
|
||||
- [ ] **Step 1: Kopf prüfen und umbauen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "subtitle" src/components/ui/PageHeader.jsx`
|
||||
Expected: `subtitle` steht in `.sub` und nimmt einen Knoten an. Die Abzeichen sind `span` und damit auch in einem `<p>` gültig.
|
||||
|
||||
| Element | Umsetzung |
|
||||
|---|---|
|
||||
| Laden | `<div className="page"><div className="loading">Wird geladen …</div></div>` |
|
||||
| Nicht gefunden | `EmptyState title="Kunde nicht gefunden" hint={error}` mit `Button as={Link} to="/customers"` „Zurück“ (ohne Pfeil) |
|
||||
| Kopf | `crumb={[{ label: 'Kunden', to: '/customers' }, { label: customerTitle(customer) }]}`, `eyebrow={customerEyebrow(customer)}`, `title={customerTitle(customer)}`. `.sub`: Abzeichen aus `customerBadges(customer, { stage: false })`, dann `[location, email, phone].filter(Boolean).join(' · ')`, beides leer: kein `.sub` |
|
||||
| Aktionen | `Button variant="danger"` „Kunde löschen“ (nur Admin), `Button` „Wiederherstellen“ bzw. „Archivieren“, `KundenansichtButton`. `removeCustomer` (mit `window.confirm`/`window.alert`) und `toggleArchived` bleiben |
|
||||
|
||||
`KundenansichtButton`: Icon und Spinner entfallen, der Text „Kundenansicht öffnen“ bleibt, bei `busy` steht „Wird geladen …“. `title`, die `disabled`-Regel, `window.alert` und `openInNewTab` bleiben. `PortalAccessCard` übergibt keine `variant` mehr.
|
||||
|
||||
Reiter: `Tabs tabs={customerTabs(isAdmin)}`, `activeTab`, `?tab=` und `setSearchParams(…, { replace: true })` bleiben. Die neun Reiter:
|
||||
|
||||
| id | Label | Inhalt |
|
||||
|---|---|---|
|
||||
| `overview` | Übersicht | `OverviewTab` (P4) |
|
||||
| `tickets` | Tickets | `CustomerTickets` (P4) |
|
||||
| `invoices` | Rechnungen (nur Admins) | `CustomerInvoices` unverändert (P7) |
|
||||
| `projects` | Projekte | `CustomerProjects` mit `AssignedProjectCard` (P5) |
|
||||
| `chat` | Chat | `<Card><ChatThread …/></Card>` unverändert (P6) |
|
||||
| `abo` | Abo & Angebote | `AboStatusCard` (Task 7) und `OffersView` (P6), ohne Umschlag mit Inline-Style |
|
||||
| `gutschriften` | Gutschriften | `CreditsView` unverändert (P6) |
|
||||
| `funktionen` | Funktionen | `CustomerFunktionen` (Task 6) |
|
||||
| `protokoll` | Protokoll | `CustomerViewLog` (Task 7) |
|
||||
|
||||
- [ ] **Step 2: Übersicht**
|
||||
|
||||
Wurzel `<div className="two-col">` mit zwei `<div>`-Spalten. Links `Card title="Stammdaten"` und `Card title="Rechnungsdaten"`, rechts `PortalAccessCard`. State und Handler (`save`, `upgrade`, `savePassword`) bleiben, ebenso alle Texte und dass der Name `companyName` mitschreibt. Aus den `Field`-Zeilen werden `.kv`-Zeilen (`<dt><label htmlFor>`, `<dd>` mit `className="input"`), jedes Feld bekommt eine `id`.
|
||||
|
||||
| Abschnitt | Inhalt in dieser Reihenfolge |
|
||||
|---|---|
|
||||
| Stammdaten | Unternehmen / Name, Code, E-Mail (`type="email"`), Telefon, Standort, Ansprechpartner, Notizen (`<textarea className="textarea" rows={3}>`). Darunter `<label className="check">` „Wichtiger Kunde“, dann `.toolbar` mit `Button variant="primary"` „Speichern“ (bei `busy` „Speichert …“), bei Leads `Button` „Zum festen Kunden machen“ und der Meldung |
|
||||
| Rechnungsdaten | oben `<Badge tone="warn">Adresse unvollständig</Badge>` bei `!ready`, dann Strasse + Nr., PLZ, Stadt, Land, USt-IdNr. |
|
||||
| Portal-Zugang | Kundenportal (`Badge ok` „freigeschaltet“ bzw. `muted` „kein Zugang“), Letzter Login („…“, „nicht abrufbar“ oder `formatLastLogin`), Login (E-Mail) und Passwort als `CopyableCredential`, das Feld „Neues Passwort setzen“ bzw. „Passwort festlegen (min. 8 Zeichen)“, `.toolbar` mit `Button` „Passwort ändern“ bzw. „Zugang anlegen“ (bei `busy` „Speichert …“), `KundenansichtButton`, Meldung, zuletzt der Portal-Hinweis als `<p className="muted small">` |
|
||||
|
||||
Meldungen: `msg` wird `{ tone, text }` mit den bisherigen Texten. Fehler und Pflichthinweise sind `err` bzw. `warn` (mit `role="alert"`), Erfolge `ok`.
|
||||
|
||||
- [ ] **Step 3: Tickets- und Projekte-Reiter**
|
||||
|
||||
- `CustomerTickets`: Abfrage und Zustände bleiben. Laden `.loading`, leer `EmptyState title="Keine Tickets" hint="Fuer diesen Kunden gibt es noch keine Tickets."`.
|
||||
- Tabelle „Ticket“ und „Status“:
|
||||
- Erste Zelle `<div className="cell-title">` mit `<Link className="row-link" to={ticketPath(ticketRef(t))}>`, darin `<span className="id">{ticketNumber(t)}</span>` und der Betreff (`topic || title || '-'`), daneben `UrgentPill`. Darunter `.cell-sub` mit `t.type`. Die Kurznummer ist nie Linkziel.
|
||||
- Zweite Zelle `StatusPill`. `<tr className="row">` mit `isRowActivation` wie in Task 4.
|
||||
- `CustomerProjects`: Laden `.loading`, leer wie heute, sonst `Card title={`Projekte (${n})`}` mit `AssignedProjectCard`.
|
||||
|
||||
- [ ] **Step 4: CSS-Block „Kunden (P4)“**
|
||||
|
||||
Der Block steht hinter den Bausteinen und vor dem KOMPAT-Block und beginnt mit `/* ===== Kunden (P4) ===== */`:
|
||||
- `.kv .textarea { width: 100%; max-width: 360px }` (die Prototypregel gilt nur für `.input`)
|
||||
|
||||
Den Abstand zwischen aufeinanderfolgenden Abschnitten in den Spalten (`h2.section`) liefert P1 mit `.page section + section { margin-top: 36px }`. P4 baut dafür keine eigene Regel.
|
||||
|
||||
- [ ] **Step 5: Hygiene und Prüfung**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|badge badge-|\bdot\b" src/pages/CustomerDetailPage.jsx src/components/customer/KundenansichtButton.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t5" --only kunde-overview,kunde-tickets,kunde-projects,kunde-lead,kunde-archiv,kunde-altdaten,kunde-nicht-gefunden,kunde-chat,kunde-invoices,kunde-gutschriften --gate p4`
|
||||
Expected: Exit 0. `kunde-overview` zeigt zwei Spalten mit `.kv`, der Abstand zwischen Stammdaten und Rechnungsdaten beträgt mindestens 24 px (36 px aus P1), der Kopf hat keinen schwarzen Knopf. Bei 390 px stehen die Spalten untereinander, und die Reiter scrollen waagerecht.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/CustomerDetailPage.jsx src/components/customer/KundenansichtButton.jsx src/styles/global.css
|
||||
git commit -m "Kundenseite: Brotkrumen, Eyebrow, Uebersicht in zwei Spalten, Tickets-Reiter mit Links" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Funktionen als Tabelle
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/customer/CustomerFunktionen.jsx` (Zeilen 1–130, die Logik bleibt)
|
||||
- Modify: `src/styles/global.css` (Block „Kunden (P4)“ aus Task 5: Funktionen-Regeln)
|
||||
- Modify: `src/styles/admin-areas.css` (Funktionen-Regeln löschen: Zeilen 24–32, 36 und 60)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `FUNKTIONEN`, `ZUSTAND`, `funktionOf`, `aktionen`, `mergeFunktion`, `siteKeyOf` und, nur in Fall A (Task 1 Step 4), `abgeschaltetText` aus `lib/funktionenView` (Stand von `test`, nicht geändert). `copyText` aus `CopyableCredential`. `getFunktionen`, `setFeature`. `ticketPath` (P2).
|
||||
- Produces: `CustomerFunktionen({ customerId })`, gleiche Signatur.
|
||||
|
||||
- [ ] **Step 1: Markup**
|
||||
|
||||
State, `load`, `save`, `ask` und `short` bleiben. Jede Bedienung von heute bleibt erreichbar, alle Texte bleiben. Der Import von `admin-areas.css` entfällt, die Funktionen-Regeln wandern nach `global.css`, damit P6 die Datei ohne P4-Nutzer löschen kann.
|
||||
|
||||
| Element | Umsetzung |
|
||||
|---|---|
|
||||
| Zustände | Fehler `<div className="msg err" role="alert">`. Laden `.loading` „Wird geladen …“. Leer `EmptyState title="Keine Projekte für diesen Kunden."` |
|
||||
| Rahmen | `Card title="Funktionen"` mit `<table className="table funktionen-table">`, Kopf „Funktion“, „Status“, „Ticket“, „Aktionen“ |
|
||||
| Projektzeile | je Projekt ein `<tbody>`, dessen erste Zeile `<tr className="funktionen-group"><th colSpan={4} scope="rowgroup">` den Projektnamen trägt. Dahinter `liveUrl` ohne Protokoll als `<a className="link" target="_blank" rel="noreferrer">` |
|
||||
| Funktionszeile | je Funktion: `f.label`, Status, Ticket, Aktionen |
|
||||
| Status | `Badge tone={tone}` mit `label`. Darunter (Fall A) `.cell-sub` mit `abgeschaltetText(feature)` (wenn nicht leer), `.cell-sub` mit `short(feature.adminNote)` und `title={feature.adminNote}` und bei `siteKeyOf(...)` der `SiteKey` |
|
||||
| `SiteKey` | `.funktionen-sitekey` mit „Site-Key“ (`.muted`), `<code>` und `<button type="button" className="link">` „Kopieren“ bzw. „Kopiert“ (1,8 s). `title` ist „Site-Key kopieren“ bzw. „Kopiert“, `aria-label` „Site-Key kopieren“ bzw. „Site-Key kopiert“, beides wie heute. Kein Icon |
|
||||
| Ticket | `ticketMissing` → `<Badge tone="danger">Ticket fehlt</Badge>`. Mit `ticketWoid` → `<Link className="link" to={ticketPath(feature.ticketWoid)}>#{feature.ticketWoid}</Link>`. Sonst `<span className="faint">–</span>` |
|
||||
| Aktionen | je `aktionen(feature.state)` ein `<button type="button" className="link">` „Aktivieren (ohne Zahlung)“ bzw. „Abschalten“ in `.funktionen-actions`. Solange die Bestätigung dieser Zeile offen ist, bleibt die Zelle leer |
|
||||
| Bestätigung | Direkt unter der Funktionszeile `<tr className="funktionen-confirm"><td colSpan={4}>` mit einem `.flex.items-center.gap-2.wrap`: Text `f.bestaetigen[confirm.action]`, `<input className="input funktionen-note" placeholder="Notiz (optional)" maxLength={120} aria-label="Notiz (optional)">`, `Button variant="primary"` „Ja“ (bei `busy` „Speichert …“, `disabled`) und `Button` „Abbrechen“ (`disabled` bei `busy`) |
|
||||
| Rückmeldung | unter der Tabelle `<div className={`msg ${status.ok ? 'ok' : 'err'}`}>`, bei Fehler mit `role="alert"` |
|
||||
| tp2-Zusatz (nur Fall A) | Die Datei enthält zusätzlich `KopierKnopf`, `AnleitungPanel`, `TextformPanel`, den Knopf „Anleitung“ (`aria-expanded`) und `hindernisText` beim Status. Logik, Zustände und Texte bleiben. Umbau wie oben: Knöpfe ohne `size` und Icons (`Button type="button"` bzw. `button.link`), der Spinner wird „Wird geladen …“, Kopierknöpfe zeigen „Kopiert“ bzw. ihren Namen, ein Icon-Knopf zeigt seinen `aria-label`-Text, `form-control` wird `input`, Inline-Styles werden Klassen im Block „Kunden (P4)“, `.area-*` wird `.msg err`, `.msg warn` bzw. `.muted`. Die Panels stehen als `<tr className="funktionen-extra"><td colSpan={4}>` direkt unter der Funktionszeile, `<details>` bleibt |
|
||||
|
||||
- [ ] **Step 2: CSS verschieben**
|
||||
|
||||
Aus `admin-areas.css` entfallen alle `.funktionen-*`-Regeln samt `.funktionen-leer` in den beiden Responsive-Blöcken. In Fall A ziehen auch die `.funktionen-panel*`-Regeln von tp2 um, mit Tokens statt `--surface-*` und `--border` (`--bg-subtle`, `--line`). Im Block „Kunden (P4)“ von `global.css` stehen:
|
||||
|
||||
| Selektor | Wert |
|
||||
|---|---|
|
||||
| `.funktionen-group th` | `text-align: left; font-size: 14px; font-weight: 600; color: var(--text); padding-top: 20px; overflow-wrap: anywhere` |
|
||||
| `.funktionen-group .link` | `font-size: 13px; font-weight: 400; margin-left: 8px` |
|
||||
| `.funktionen-sitekey` | `display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin-top: 4px` |
|
||||
| `.funktionen-sitekey code` | `font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; overflow-wrap: anywhere; user-select: all` |
|
||||
| `.funktionen-actions` | `display: flex; flex-wrap: wrap; gap: 4px 14px` |
|
||||
| `.funktionen-confirm td` | `padding-top: 0` |
|
||||
| `.funktionen-note` | `width: 100%; max-width: 260px` |
|
||||
|
||||
- [ ] **Step 3: Hygiene und Prüfung**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|area-|admin-areas|ui-btn|size=" src/components/customer/CustomerFunktionen.jsx; rg -n "funktionen" src/styles/admin-areas.css`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t6" --only kunde-funktionen,fehler-kunde-funktionen --gate p4`
|
||||
Expected: Exit 0. Das Bild von `kunde-funktionen` zeigt die geöffnete Bestätigungszeile. Bei 390 px läuft nichts über (Site-Key und lange URL brechen um), in Fall A ist „Abgeschaltet (Abo beendet), Löschung am 04.11.2026“ lesbar, und jeder Textlink zeigt den Fokusring.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/customer/CustomerFunktionen.jsx src/styles/global.css src/styles/admin-areas.css
|
||||
git commit -m "Kunden-Reiter Funktionen als Tabelle mit Textaktionen, Bestaetigung als eigene Zeile" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: Abo-Status, Protokoll und Grundstil von `admin-areas.css`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/customer/AboStatusCard.jsx` (Zeilen 1–44)
|
||||
- Modify: `src/components/customer/CustomerViewLog.jsx` (Zeilen 1–56)
|
||||
- Modify: `src/styles/admin-areas.css` (Zeilen 1–61)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Card`, `Badge`, `Button`, `EmptyState`. `getCustomerAboStatus`, `getCustomerViewLog`. `formatViewLogTime`, `viewLogLabel`.
|
||||
- Produces: den Grundstil der `.area-*`-Klassen für die P6-Seiten. Die P4-Dateien benutzen keine `.area-*`-Klasse und keinen Import von `admin-areas.css` mehr.
|
||||
|
||||
- [ ] **Step 1: `AboStatusCard`**
|
||||
|
||||
`Card title="Abo"`. Der Import von `admin-areas.css` entfällt. Laden `.loading`, Fehler `<div className="msg err" role="alert">`, ohne Abo `<p className="muted">kein Abo</p>`. Die Zeilen werden `<dl className="kv">`: „Abo“ (Produktname plus `Badge`, im `<dd className="flex items-center gap-2 wrap">`), „Leistungsbeginn“, „Nächste Abbuchung“ (nur bei `abo.active`), „Gekündigt zum“, „Zusatzspeicher“. Texte und Bedingungen wie heute.
|
||||
|
||||
- [ ] **Step 2: `CustomerViewLog`**
|
||||
|
||||
- Fehler: `.msg.err` mit der Meldung und daneben `Button` „Erneut versuchen“ (ohne Icon, `onClick={load}`), beides in einem `.flex.items-center.gap-2.wrap`. Laden `.loading`. Leer wie heute (`EmptyState title="Noch keine Einträge"`).
|
||||
- Tabelle „Zeit“ (`formatViewLogTime(e.at)`, `nowrap num`), „Mitarbeiter“ (`e.staffName || '–'`), „Aktion“ (`viewLogLabel(e.action)`, darunter `.cell-sub` mit `e.detail`). Keine Zeilenaktion, keine Links.
|
||||
|
||||
- [ ] **Step 3: Grundstil von `admin-areas.css`**
|
||||
|
||||
| Regel | Neu |
|
||||
|---|---|
|
||||
| `.area-row` | `padding: 12px 0` statt 12px 16px, Haarlinie `var(--line)` |
|
||||
| `.area-row.head` | `color: var(--text-2); font-size: 12px; font-weight: 500`, ohne `text-transform` und `letter-spacing` |
|
||||
| `.area-row.active` | `background: var(--bg-subtle)` |
|
||||
| `.area-muted`, `.area-danger`, `.area-warn` | 13 px in `var(--text-2)`, `var(--red-fg)`, `var(--orange-fg)` |
|
||||
| `.area-summary` | ohne Hintergrund, Radius und Innenabstand, `margin-bottom: 12px`, `.warn` färbt die Schrift `var(--orange-fg)` |
|
||||
| `.area-message`, `.area-error` | wie `.msg.ok` und `.msg.err`: 13 px, `var(--green-fg)` bzw. `var(--red-fg)`, ohne Kasten, Abstand `8px 0` |
|
||||
| `.area-kv dt`, `.credit-calc label`, `.credit-field` | `color: var(--text-2)` |
|
||||
| `.area-rows a`, `.area-rows a:hover`, `.credit-cols a` | entfallen. Links ohne Klasse bekommen Farbe und feinen Unterstrich von `a:not([class])` (P1). `admin-areas.css` lädt vor `global.css`, eine eigene Regel mit gleicher Spezifität würde dort verlieren |
|
||||
| Responsive | Beide Blöcke (`@media (max-width: 720px)` wird `860px`, und `@container (max-width: 960px)`) stehen am Dateiende hinter allen Spaltenregeln, `!important` entfällt in beiden. Bei gleicher Spezifität gewinnt die spätere Regel. Hat ein Spaltenselektor mehr als eine Klasse, bekommt der Responsive-Selektor dieselben Klassen |
|
||||
| Bleiben bis P6 | `.storage-row`, `.credit-row`, `.credit-cols`, `.credit-*`, `.offer-row`, `.area-filter`, `.area-inline`, `.area-kv` |
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "!important|#[0-9a-fA-F]{3,6}\b|rgba\(|--text-muted|--text-faint|--surface|--border|--r-sm|--ok\b|--accent" src/styles/admin-areas.css`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 4: Hygiene und Prüfung**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{\{|form-control|ui-card|area-|admin-areas" src/components/customer`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4-t7" --only kunde-abo,kunde-protokoll,fehler-kunde-protokoll,fehler-kunde-abo,kunde-gutschriften,gutschriften,angebote,speicher --gate p4`
|
||||
Expected: Exit 0. Auf `gutschriften`, `angebote` und `speicher` stehen die Zeilen mit Haarlinien ohne Kästen und die Kopfzeilen ohne Großbuchstaben. Sie bleiben lesbar, P6 ersetzt sie.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/customer/AboStatusCard.jsx src/components/customer/CustomerViewLog.jsx src/styles/admin-areas.css
|
||||
git commit -m "Abo-Status und Protokoll ohne Karten und Icons, neuer Grundstil fuer admin-areas.css" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: Gate P4, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0, `# fail 0`, `# pass` = N3 + 6. Liegen neue Commits auf `test`, kann die Zahl höher sein; sie wird notiert.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p4 && "$W/shots.sh" p4 "$W/shots/p4" --gate p4`
|
||||
Expected:
|
||||
- Gate `p4` Exit 0.
|
||||
- Keine Verstöße gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request` und `step-failed` auf allen Ansichten, auch auf `gutschriften`, `angebote` und `speicher`.
|
||||
- Die Kundenansichten aus dem `icon-svg`-Umfang haben kein `icon-svg` außerhalb `.credential`.
|
||||
|
||||
- [ ] **Step 3: Code-Hygiene im umgebauten Bereich**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{\{|/tickets\?woid" src/pages/CustomersPage.jsx src/pages/CustomerDetailPage.jsx src/components/customer; rg -w side-logo src`
|
||||
Expected: keine Treffer. `AssignProjectPage.jsx` behält seine Icons bis P5.
|
||||
|
||||
- [ ] **Step 4: Bundle-Größe**
|
||||
|
||||
Die Größen stehen in der Ausgabe von `check.sh` (Step 1). Sie werden mit `design-p3` verglichen. Das JS wird kleiner (Icon-Importe in sieben Dateien entfallen).
|
||||
|
||||
- [ ] **Step 5: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p3..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P4. Schwerpunkte:
|
||||
- Texte unverändert (Diff-Stichprobe der Formulare und Meldungen)
|
||||
- Zeilenklick-Logik und erste Zelle als echter Link
|
||||
- Dialog-Verdrahtung (`busy`, Esc, `onClose`)
|
||||
- `ticketPath` als einziges Linkziel auf Tickets
|
||||
- `admin-areas.css` ohne `!important`
|
||||
- `lib/leads.js` exportiert weiter alle bisherigen Namen (`git diff design-p3 -- src/lib/leads.js`)
|
||||
- jede Bedienung aus Task 4 bis 7 ist erreichbar (Kopfknöpfe, neun Reiter, beide Formulare, Aktivieren, Abschalten, Bestätigung mit Notiz, Site-Key kopieren, „Erneut versuchen“)
|
||||
- „Abbrechen“ in beiden Anlege-Dialogen hat `type="button"`
|
||||
|
||||
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only kunden,kunde-overview,dlg-kunde-neu`.
|
||||
|
||||
- [ ] **Step 6: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p4 && git log --oneline origin/test..design-p4 && git rev-parse design-p4`
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- die Screenshots aus `shots/p4/1440x900` und `shots/p4/390x844` (Auswahl: `kunden`, `kunden-altdaten-archiv`, `dlg-kunde-neu`, `kunde-overview`, `kunde-tickets`, `kunde-funktionen`, `kunde-protokoll`) und den Vorher-Vergleich aus `shots/p4-vorher`
|
||||
- den Hinweis: „Fester Kunde“ gibt es in der Liste nicht mehr als Abzeichen, „Wichtig“ ersetzt den Stern, die Suche findet jetzt auch den Ansprechpartner
|
||||
- den tp2-Stand aus Task 1: Fall A (tp2 war vor P4 in `test`, das Anleitungs- und Zustimmungs-Panel ist in die Zusatzzeilen der Tabelle übernommen) oder Fall B (P4 ging vor tp2, die Konflikte in `CustomerFunktionen.jsx`, `lib/funktionenView.js` und `admin-areas.css` löst die vom Eigentümer genannte Stelle beim Rebase von tp2 auf, die Panels müssen dann in die Zusatzzeile `funktionen-extra` der Tabelle)
|
||||
- Notizen für spätere Pakete:
|
||||
- P5: `CopyableCredential` und `AssignedProjectCard` erscheinen im Portal-Zugang und im Projekte-Reiter noch in alter Form, die Icons von `AssignProjectPage.jsx` sind unberührt.
|
||||
- P6: `KundenansichtButton` ist jetzt ein Standardknopf, in `CreditDetail` bleibt „Genehmigen“ der einzige schwarze Knopf. Die Raster `.storage-row`, `.credit-row`, `.offer-row` und `.area-kv` stehen noch in `admin-areas.css`.
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p4^{commit}:refs/heads/test'`
|
||||
- die Rücknahme: `git revert --no-commit design-p3..design-p4`, Commit und Push durch den Eigentümer (gilt nur, solange P4 das jüngste Paket ist)
|
||||
|
||||
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers: Kundenliste mit Reitern, eine Kundenseite mit Tickets-Reiter und Funktionen.
|
||||
@@ -0,0 +1,899 @@
|
||||
# WOMS „Weißes Design“ P5: Projekte – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P5 steht der Projektbereich im neuen Design. Die Projektliste ist eine Tabelle mit Reitern, beide Projekt-Dialoge laufen über `Dialog`, Zuordnen-Assistent und Services bestehen aus ruhigen Abschnitten. Alle Projekt-Bausteine (am Ticket und beim Kunden) kommen ohne Karten, Icons und Farbliterale aus. Jede heutige Funktion bleibt erreichbar.
|
||||
|
||||
**Architecture:**
|
||||
- Reine Logik, die heute in `ProjectsPage.jsx` steckt (Projektart, Hosting-Zustand, Aktionen), wandert mit Tests nach `src/lib/projectRow.js`. `src/lib/projectList.js` bekommt den neuen Export `projectTabs`.
|
||||
- Die Töne der Status-Vokabulare (Preview, Analyse, Projektstatus) werden in `src/components/projectTones.js` auf Badge-Töne abgebildet. `previewEnv.js`, `previewView.js` und `analyseView.js` bleiben unverändert.
|
||||
- `global.css` bekommt am Ende den Block „Projekte (P5)“. Die Komponenten benutzen nur Bausteine aus P1.
|
||||
|
||||
**Tech Stack:** React 18, react-router-dom 6, date-fns 2 (`formatDistance`, Locale `de`, wie schon in `TicketCard`), reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Bausteine“ (Tabelle, Liste, Dialog, Badge, Reiter, Meldungen, Laden), „Ticket als eigene Seite / Route und Links“ (Ticket-Spalte), „Texte“, „Was gleich bleibt“ und Paket P5. Dazu der Anhang `docs/superpowers/specs/2026-10-04-weisses-design-umbaukarte.md` (Abschnitte 3, 4, 5.P5, 7). Globale Regeln und gemeinsame Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Screenshot-Werkzeug) und `$TMPDIR`. Zeilenangaben beziehen sich auf den Stand `603d08c` und sind nach P1 bis P4 verschoben. Maßgeblich ist der Inhalt.
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P5 kommt hinzu:
|
||||
|
||||
- **Vorbedingung:** Die Tags `design-p1` bis `design-p4` existieren. Task 0 prüft das und rebased auf `origin/test`.
|
||||
- **Dateien dieses Pakets:** `ProjectsPage`, `AssignProjectPage`, `ServicesPage`, `TicketProjects`, `AssignedProjectCard`, `AnalyseRow`, `PreviewBlock`, `PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential`, dazu `global.css` und `AdminPage.css` (nur `.credential*`).
|
||||
- **Unverändert bleiben:** alle Hooks, `lib/previewEnv.js`, `lib/previewView.js`, `lib/analyseView.js`, `lib/projectAssign.js`, `lib/assignFlow.js`, `lib/servicesView.js`, `lib/projectAdminApi.js` und die Signaturen von `AssignedProjectCard`, `TicketProjects`, `AnalyseRow`, `PreviewBlock`, `CopyableCredential`, `copyText`, `getCustomerPortalPassword`. `lib/projectList.js` bekommt nur einen neuen Export.
|
||||
- **Texte:** wörtlich wie heute (auch die ae/ue-Schreibung wie „Oeffentliche“ oder „angestossen“). Neu sind nur die Texte der Spec-Liste „Texte“ und die mit „Entscheidung:“ markierten Zeilen dieses Plans.
|
||||
- **Status-Vokabulare** (`previewStatus().color`, `analyseStatus().color`, `previewState`, `status`, `provisioningStatus`) werden nicht geändert. Die Tonabbildung liegt in `projectTones.js`.
|
||||
- **Jeder `Button` und jeder Textlink-Knopf (`button.link`) bekommt ein explizites `type`** (`button` oder `submit`), weil P1 keinen Standard setzt. In einem `<Dialog as="form">` trägt jeder Knopf außer dem Absenden-Knopf `type="button"`.
|
||||
- **Zeilenklick:** Klickbare Tabellenzeilen prüfen Klicks mit `isRowActivation(event)` aus `src/lib/rowClick.js` (P2, Task 2). P5 baut keinen eigenen Helfer.
|
||||
- **Abschnittsabstände:** Zwischen aufeinanderfolgenden Abschnitten (`Card`) sorgt `.page section + section` (P1). P5 hat dafür keine eigene Regel.
|
||||
- **Keine `style={{…}}`:** P5 hat keine Laufzeitwerte. Kein Farbliteral, kein `btn-sm`, keine Icon-Importe in den Dateien dieses Pakets.
|
||||
- **`window.confirm` und `window.alert`** bleiben an ihren Stellen.
|
||||
- **Ticketlinks** über `ticketPath` aus `src/lib/ticketRoutes.js` (P2). Die drei Stellen in `AssignProjectPage` (2×) und `ServicesPage` sind seit P2 umgestellt und bleiben es.
|
||||
- **Testzahl:** Stand `design-p4` plus 13 neue Tests (7 in `projectRow`, 2 in `projectList`, 4 in `projectTones`).
|
||||
|
||||
## Review Focus
|
||||
|
||||
Aus der Übersicht gehören vier Punkte zu P5. Die Prüfungen laufen als Ansichten im Gate `p5` (Task 4).
|
||||
|
||||
- **Punkt 2, lange Inhalte am Handy:** Ansicht `projekte-lang` (Projektname 120 Zeichen, Repo-Pfad 90 Zeichen, Kundenname 60 Zeichen). Dazu `projekte-861` und `projekte-1101` (Menü fest, aber wenig Platz). Erwartet: kein `overflow-x`, die erste Spalte bleibt (gerundet) mindestens 140 px breit.
|
||||
- **Punkt 3, Teilausfall:** Ansicht `fehler-projekte` (Mitarbeiterliste antwortet mit 500). Erwartet: Tabelle und Reiter bedienbar, Mitarbeiter-Auswahl zeigt nur „Alle Mitarbeiter“.
|
||||
- **Punkt 4, nur Tastatur:**
|
||||
- Das `<button class="link">` der ersten Zelle ist per Tab erreichbar, Enter öffnet „Projekt bearbeiten“, Esc schließt.
|
||||
- Während Speichern ist „ד gesperrt und Esc wirkungslos.
|
||||
- Enter im Namensfeld speichert, Enter in den Feldern des `PreviewBlock` speichert nie.
|
||||
- Klicks auf Link, Auswahl und Knopf in einer Zeile öffnen den Dialog nicht.
|
||||
- **Punkt 5, viele Einträge:** Ansicht `projekte-viel` (300 Projekte, 40 Kunden) rendert ohne Verstoß, der Bearbeiten-Dialog öffnet.
|
||||
- **Altdaten:** Projekt ohne Namen („(ohne Name)“), Ticket-`$id` ohne Treffer in der Ticketliste (Rohwert als Link), `$updatedAt` unlesbar oder leer („-“), Status leer („-“). Sie stehen in `projekte-lang`.
|
||||
- **Kein Flackern:** Nach Hosten, Aktualisieren, Kundenwechsel und Speichern bleibt die Tabelle stehen. Der Text „Wird geladen …“ erscheint nur beim ersten Laden.
|
||||
- **Jede heutige Steuerung ist erreichbar:** Die Zuordnungen stehen in den Tabellen der Tasks 7 bis 13. Task 14 prüft sie anhand der Ansichten.
|
||||
|
||||
---
|
||||
|
||||
### Task 0: Paketstart
|
||||
|
||||
- [ ] **Step 1: Stand und Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p4 HEAD && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0 und `# fail 0`. Die Zahl `# pass` wird notiert (Basis für Task 14). Ist `design-p4` kein Vorfahre von `HEAD`, stoppt der Agent und meldet es dem Eigentümer.
|
||||
|
||||
- [ ] **Step 2: Parallele Arbeit an TP2**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/ProjectsPage.jsx src/pages/AssignProjectPage.jsx src/pages/ServicesPage.jsx src/components/TicketProjects.jsx src/components/AssignedProjectCard.jsx src/components/AnalyseRow.jsx src/components/PreviewBlock.jsx src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/CopyableCredential.jsx src/pages/AdminPage.css src/styles/global.css src/lib/projectList.js`
|
||||
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
|
||||
|
||||
- [ ] **Step 3: Bausteine aus P2**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/lib/rowClick.js src/lib/ticketRoutes.js && rg -n "isRowActivation" src/lib/rowClick.js`
|
||||
Expected: Beide Dateien sind da, `isRowActivation` steht in `rowClick.js` (P2, Task 2). Fehlt etwas, stoppt der Agent.
|
||||
|
||||
### Task 1: Projekt-Helfer `projectRow.js`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/projectRow.js`
|
||||
- Test: `tests/unit/projectRow.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `fmtDateTime(value)` aus `src/lib/format.js` (liefert `05.10.26, 09:00`), `formatDistance` aus `date-fns`, `de` aus `date-fns/locale`.
|
||||
- Produces (1:1 aus `ProjectsPage.jsx` L35-85 herausgezogen, Töne statt `badge-*`-Klassen):
|
||||
- `PROJECT_TYPES`: `{ preview, website, template, project }`, je `{ label, short, tone, hint, adminOnly? }`. Die Reihenfolge der Schlüssel bestimmt die Optionen im Dialog.
|
||||
- `HOSTED_TYPES`: `Set(['preview','website','template'])`.
|
||||
- `typeOf(project) → 'template' | 'website' | 'preview' | 'project'`.
|
||||
- `isProjectReady(project) → boolean`.
|
||||
- `projectActions(project) → { type, hosted, ready, host, preview, refresh, gitea, hostedBadge }`.
|
||||
- `projectTicketRef(project, ticketMap) → string`.
|
||||
- `updatedLabel(iso, now = new Date()) → { text, title }`.
|
||||
- Den Zeilenklick-Helfer `isRowActivation(event)` liefert `src/lib/rowClick.js` (P2, Task 2). `projectRow.js` baut keinen eigenen.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
```js
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { PROJECT_TYPES, typeOf, isProjectReady, projectActions, projectTicketRef, updatedLabel } = await loadModule('src/lib/projectRow.js')
|
||||
|
||||
test('typeOf: Vorlage vor Website vor Preview vor Projekt', () => {
|
||||
assert.equal(typeOf({ isTemplate: true, isPublic: true, subdomain: 'x' }), 'template')
|
||||
assert.equal(typeOf({ isPublic: true, subdomain: 'x' }), 'website')
|
||||
assert.equal(typeOf({ subdomain: 'x' }), 'preview')
|
||||
assert.equal(typeOf({}), 'project')
|
||||
})
|
||||
test('isProjectReady: nur ready/deployed, provisioningStatus hat Vorrang', () => {
|
||||
assert.equal(isProjectReady({ status: 'ready' }), true)
|
||||
assert.equal(isProjectReady({ status: 'DEPLOYED' }), true)
|
||||
assert.equal(isProjectReady({ provisioningStatus: 'pending', status: 'ready' }), false)
|
||||
assert.equal(isProjectReady({ status: 'building' }), false)
|
||||
assert.equal(isProjectReady({}), false)
|
||||
})
|
||||
test('PROJECT_TYPES: Reihenfolge, Texte und Töne unverändert', () => {
|
||||
assert.deepEqual(Object.keys(PROJECT_TYPES), ['preview', 'website', 'template', 'project'])
|
||||
assert.deepEqual(Object.values(PROJECT_TYPES).map((t) => t.label), ['Preview (mit Login)', 'Oeffentliche Website', 'Vorlage', 'Internes Projekt (kein Hosting)'])
|
||||
assert.deepEqual(Object.values(PROJECT_TYPES).map((t) => t.short), ['Preview', 'Website', 'Vorlage', 'Projekt'])
|
||||
assert.deepEqual(Object.values(PROJECT_TYPES).map((t) => t.tone), ['info', 'ok', 'warn', 'muted'])
|
||||
assert.equal(PROJECT_TYPES.template.adminOnly, true)
|
||||
assert.equal(PROJECT_TYPES.project.hint, 'Nur Gitea-Repository, keine Website/Subdomain')
|
||||
})
|
||||
test('projectActions: gehostet und nicht deployt', () => {
|
||||
const ready = projectActions({ subdomain: 'a', status: 'ready', previewUrl: 'https://a.project.webklar.com', giteaRepoUrl: 'https://git/x' })
|
||||
assert.deepEqual(ready, { type: 'preview', hosted: true, ready: true, host: false, preview: true, refresh: true, gitea: true, hostedBadge: { text: 'gehostet', tone: 'ok' } })
|
||||
const open = projectActions({ subdomain: 'a', status: 'provisioning', previewUrl: 'https://a.project.webklar.com' })
|
||||
assert.deepEqual(open, { type: 'preview', hosted: true, ready: false, host: true, preview: false, refresh: false, gitea: false, hostedBadge: { text: 'nicht deployt', tone: 'warn' } })
|
||||
})
|
||||
test('projectActions: archiviert, ohne Website, Vorlage', () => {
|
||||
const arch = projectActions({ subdomain: 'a', status: 'ready', archived: true, previewUrl: 'u' })
|
||||
assert.deepEqual([arch.host, arch.preview, arch.refresh, arch.hostedBadge], [false, true, false, null])
|
||||
const archOpen = projectActions({ subdomain: 'a', status: 'error', archived: true })
|
||||
assert.deepEqual([archOpen.host, archOpen.refresh, archOpen.hostedBadge], [false, false, null])
|
||||
const intern = projectActions({ giteaRepoUrl: 'https://git/x' })
|
||||
assert.deepEqual([intern.type, intern.host, intern.preview, intern.refresh, intern.hostedBadge, intern.gitea], ['project', false, false, false, null, true])
|
||||
const tpl = projectActions({ isTemplate: true, subdomain: 't', status: 'deployed' })
|
||||
assert.deepEqual([tpl.type, tpl.refresh, tpl.host], ['template', true, false])
|
||||
})
|
||||
test('projectTicketRef: woid aus der Liste, sonst die $id, ohne Ticket leer', () => {
|
||||
assert.equal(projectTicketRef({ ticketId: 'abc' }, { abc: '100131' }), '100131')
|
||||
assert.equal(projectTicketRef({ ticketId: 'abc' }, { abc: 100131 }), '100131')
|
||||
assert.equal(projectTicketRef({ ticketId: 'zzz' }, {}), 'zzz')
|
||||
assert.equal(projectTicketRef({ ticketId: '' }, { abc: '1' }), '')
|
||||
assert.equal(projectTicketRef({}, {}), '')
|
||||
})
|
||||
test('updatedLabel: deutsch relativ, Titel in Berliner Zeit, ungültig = "-"', () => {
|
||||
const now = new Date('2026-10-05T10:00:00.000Z')
|
||||
assert.deepEqual(updatedLabel('2026-10-05T07:00:00.000Z', now), { text: 'vor etwa 3 Stunden', title: '05.10.26, 09:00' })
|
||||
assert.equal(updatedLabel('2026-10-04T10:00:00.000Z', now).text, 'vor 1 Tag')
|
||||
for (const bad of ['', 'kaputt', undefined]) assert.deepEqual(updatedLabel(bad, now), { text: '-', title: '' })
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectRow.test.mjs`
|
||||
Expected: FAIL, Modul `src/lib/projectRow.js` fehlt.
|
||||
|
||||
- [ ] **Step 3: `projectRow.js` implementieren**
|
||||
|
||||
- `PROJECT_TYPES`, `typeOf`, `isProjectReady` stehen wörtlich in `ProjectsPage.jsx` L38-85. Übernommen werden `label`, `short`, `hint`, `adminOnly`. `icon` entfällt. `badge: 'badge-info'` wird `tone: 'info'` (`badge-ok` → `ok`, `badge-warn` → `warn`, `badge-muted` → `muted`).
|
||||
- `projectActions`:
|
||||
- `host = hosted && !ready && !archived`
|
||||
- `preview = !host && Boolean(project.previewUrl)`
|
||||
- `refresh = hosted && ready && !archived`
|
||||
- `gitea = Boolean(project.giteaRepoUrl)`
|
||||
- `hostedBadge = null`, wenn `!hosted || archived`, sonst `{ text: 'gehostet', tone: 'ok' }` bzw. `{ text: 'nicht deployt', tone: 'warn' }`
|
||||
- `projectTicketRef`: `String(ticketMap?.[project.ticketId] ?? project.ticketId)`, bei leerer `ticketId` `''`.
|
||||
- `updatedLabel`: Text mit `formatDistance(new Date(iso), now, { addSuffix: true, locale: de })`, Titel mit `fmtDateTime(iso)`. Bei fehlendem oder unlesbarem Wert `{ text: '-', title: '' }`.
|
||||
|
||||
- [ ] **Step 4: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectRow.test.mjs`
|
||||
Expected: PASS, 7 Tests.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/projectRow.js tests/unit/projectRow.test.mjs
|
||||
git commit -m "Projekte: Typ-, Hosting- und Aktionslogik als getestete Lib-Funktionen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 2: `projectTabs`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/projectList.js` (neuer Export nach `VIEW_LABELS`)
|
||||
- Test: `tests/unit/projectList.test.mjs` (Importzeile um `projectTabs` ergänzen, zwei Tests anhängen)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `projectCounts(projects, { isAdmin })` und `VIEW_LABELS` aus derselben Datei.
|
||||
- Produces: `projectTabs(counts, { isAdmin = false }) → [{ id, label, count }]` für `Tabs` aus P1. Die IDs sind die Filterwerte der Seite (`all`, `unassigned`, `assigned`, `templates`, `archived`, `everything`).
|
||||
|
||||
- [ ] **Step 1: Failing Tests schreiben**
|
||||
|
||||
```js
|
||||
const counts = { all: 5, unassigned: 2, assigned: 3, archived: 4, templates: 1, everything: 10 }
|
||||
test('projectTabs: sechs Reiter für Admins, Wortlaut und Zähler', () => {
|
||||
assert.deepEqual(projectTabs(counts, { isAdmin: true }), [
|
||||
{ id: 'all', label: 'Aktive Projekte', count: 5 },
|
||||
{ id: 'unassigned', label: 'Unzugeordnet', count: 2 },
|
||||
{ id: 'assigned', label: 'Zugeordnet', count: 3 },
|
||||
{ id: 'templates', label: 'Vorlagen', count: 1 },
|
||||
{ id: 'archived', label: 'Archiviert', count: 4 },
|
||||
{ id: 'everything', label: 'Alle inkl. Archiv', count: 10 },
|
||||
])
|
||||
})
|
||||
test('projectTabs: ohne Admin keine Vorlagen, Zähler passen zur Ansicht', () => {
|
||||
assert.deepEqual(projectTabs(counts).map((t) => t.id), ['all', 'unassigned', 'assigned', 'archived', 'everything'])
|
||||
for (const t of projectTabs(projectCounts(P, { isAdmin: true }), { isAdmin: true })) {
|
||||
assert.equal(t.count, projectsInView(P, t.id, { isAdmin: true }).length)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
(`P` ist die Testliste am Dateianfang.)
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectList.test.mjs`
|
||||
Expected: FAIL, `projectTabs is not a function`.
|
||||
|
||||
- [ ] **Step 3: Implementieren**
|
||||
|
||||
Reihenfolge wie `FILTER_OPTIONS` in `ProjectsPage.jsx` L358-366. Labels aus `VIEW_LABELS` bzw. fest „Unzugeordnet“ und „Zugeordnet“. `templates` nur bei `isAdmin`. `VIEW_LABELS` bleibt unverändert.
|
||||
|
||||
- [ ] **Step 4: Tests grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectList.test.mjs`
|
||||
Expected: PASS, alle bisherigen plus 2 neue.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/projectList.js tests/unit/projectList.test.mjs
|
||||
git commit -m "Projekte: Reiter-Liste der sechs Ansichten als getestete Funktion" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: Tonabbildung `projectTones.js`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/projectTones.js`
|
||||
- Test: `tests/unit/projectTones.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nichts. Die Töne sind die Namen der Logik aus `Badge.jsx` (P1): `ok`, `warn`, `info`, `danger`, `muted`, `plain`.
|
||||
- Produces:
|
||||
- `previewColorTone(color)` für `previewStatus().color`.
|
||||
- `previewStateTone(state)` für `previewState` der Services-Antwort.
|
||||
- `analyseColorTone(color)` für `analyseStatus().color`.
|
||||
- `statusValueTone(value)` für `status` und `provisioningStatus` eines Projekts.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
```js
|
||||
const { previewColorTone, previewStateTone, analyseColorTone, statusValueTone } = await loadModule('src/components/projectTones.js')
|
||||
test('previewColorTone', () => {
|
||||
assert.deepEqual(['green', 'yellow', 'red', 'grey', undefined].map(previewColorTone), ['ok', 'warn', 'danger', 'plain', 'plain'])
|
||||
})
|
||||
test('previewStateTone', () => {
|
||||
assert.deepEqual(['online', 'building', 'starting', 'sleeping', 'failed', 'off', undefined, 'constructor'].map(previewStateTone), ['ok', 'warn', 'warn', 'warn', 'danger', 'plain', 'plain', 'plain'])
|
||||
})
|
||||
test('analyseColorTone', () => {
|
||||
assert.deepEqual(['green', 'red', 'grey', undefined].map(analyseColorTone), ['ok', 'danger', 'muted', 'muted'])
|
||||
})
|
||||
test('statusValueTone: wie die Farben in AssignedProjectCard heute', () => {
|
||||
assert.deepEqual(['ready', 'LIVE', 'online', 'provisioning', 'pending', 'building', 'error', 'failed'].map(statusValueTone), ['ok', 'ok', 'ok', 'warn', 'warn', 'info', 'danger', 'danger'])
|
||||
assert.deepEqual(['deployed', '', undefined, 'irgendwas'].map(statusValueTone), ['muted', 'muted', 'muted', 'muted'])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectTones.test.mjs`
|
||||
Expected: FAIL, Modul fehlt.
|
||||
|
||||
- [ ] **Step 3: Implementieren**
|
||||
|
||||
Je Funktion eine `Map` (kein Nachschlagen im Objekt-Prototyp, sonst liefert `'constructor'` eine Funktion). Die Zuordnung steht im Test. `deployed` bleibt grau, wie die Farbtabelle `STATUS_COLORS` in `AssignedProjectCard` heute (Standardfarbe `#a0aec0`).
|
||||
|
||||
- [ ] **Step 4: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/projectTones.test.mjs && "$W/check.sh" --nur-tests`
|
||||
Expected: PASS und `# fail 0` in der Ausgabe von `check.sh`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/projectTones.js tests/unit/projectTones.test.mjs
|
||||
git commit -m "Projekte: Tonabbildung fuer Preview-, Analyse- und Projektstatus" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 4: Werkzeug – Ansichten, Testdaten, Gate `p5`
|
||||
|
||||
**Files** (alle unter `$W/`, kein Commit, das Werkzeug liegt außerhalb des Repos):
|
||||
- Create: `fixtures/projekte-lang.mjs`, `fixtures/projekte-viel.mjs`
|
||||
- Modify: `views.mjs`, `gates.mjs`, bei Bedarf `fixtures/standard.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Aufbau von `fixtures/altdaten.mjs` (Standarddaten plus überschriebene Listen), `VIEWS` und `GATES` aus P0, Schritt-Funktionen `steps: async (page, ctx) => void` mit `node:assert/strict`. `ctx.backend.calls` listet alle beantworteten Requests.
|
||||
- Produces: Ansichten und Gate `p5` für die Tasks 6 bis 14.
|
||||
|
||||
- [ ] **Step 1: Testdaten anlegen**
|
||||
- `projekte-lang`: Standarddaten plus
|
||||
- Projekt A: `projectName` mit 120 Zeichen, `repoFullName` `WEBklar/` plus 82 Zeichen, Kunde mit 60 Zeichen im Namen, `ticketId` eines Tickets der Liste
|
||||
- Projekt B: ohne `projectName` und `giteaRepoName`, `ticketId` ohne Treffer in der Ticketliste, `$updatedAt: 'kaputt'`, `status: ''`, `subdomain` und `previewUrl` gesetzt, nicht deployt
|
||||
- Projekt C: archiviert, `subdomain`, `status: 'ready'`
|
||||
- `projekte-viel`: Standarddaten plus 300 Projekte und 40 Kunden.
|
||||
- `fixtures/standard.mjs` braucht ein Projekt an Ticket 100131 mit `subdomain`, `status: 'ready'`, `previewUrl` und `giteaRepoUrl`, mindestens ein Projekt je Typ (Preview, Website, Vorlage, intern) und eines ohne `kind`. Der Kunde, den `kunde-projects` öffnet, hat dieses Projekt zugeordnet. Fehlt etwas, wird es ergänzt.
|
||||
|
||||
- [ ] **Step 2: Ansichten in `views.mjs` anlegen oder anpassen**
|
||||
|
||||
`projekte`, `dlg-projekt-neu`, `dlg-projekt-bearbeiten`, `dlg-projekt-art`, `zuordnen`, `services` und `analyse-zustimmung` gibt es seit P0, `kunde-projects` und `kunde-overview` aus den Reiter-IDs der Übersicht seit P4. Dort werden nur Schritte angepasst. Alle `dlg-*` behalten `viewports` und `submitReachable` aus P0.
|
||||
|
||||
| ID | Pfad / Schritt | Testdaten |
|
||||
|---|---|---|
|
||||
| `projekte` | `/projects` | `standard` |
|
||||
| `projekte-archiv` | `/projects`, Klick auf Reiter „Archiviert“ | `standard` |
|
||||
| `projekte-treffer` | `/projects`, Suche „vorlage“ (erzeugt die Zeile „Weitere Treffer“) | `standard` |
|
||||
| `projekte-lang` | `/projects` | `projekte-lang` |
|
||||
| `projekte-viel` | `/projects` | `projekte-viel` |
|
||||
| `projekte-861`, `projekte-1101` | `/projects`, Schritt `page.setViewportSize({ width: 861 \| 1101, height: 900 })`, dann Prüfung, dass die erste Zelle der ersten Zeile (gerundet) mindestens 140 px breit ist | `projekte-lang` |
|
||||
| `projekte-aktion` | `/projects`. Ein `MutationObserver` merkt sich, ob je `.loading` im DOM erscheint. Klick auf „Aktualisieren“, 500 ms warten. Erwartung: `window.__sawLoading === false`, Tabelle sichtbar. | `standard` |
|
||||
| `projekte-tastatur` | Fokus auf das `button.link` der ersten Zeile, Enter, Dialog sichtbar, Esc, Dialog weg. Erneut öffnen, `page.route` verzögert die Speicher-Anfrage um 1200 ms (`route.fallback()`), Klick auf „Speichern & synchronisieren“. Erwartung: „ד ist `disabled`, Esc lässt den Dialog offen. Danach schließt er. | `standard` |
|
||||
| `projekte-zeilenklick` | Klick auf die Kunde-Auswahl, auf „Vorschau öffnen“ und auf ein Badge. Erwartung: nur das Badge (Zellfläche) öffnet den Dialog. | `standard` |
|
||||
| `fehler-projekte` | `/projects`, Szenario `fehler` | `standard` |
|
||||
| `dlg-projekt-neu` | Klick auf „Neues Projekt“ | `standard` |
|
||||
| `dlg-projekt-bearbeiten` | Klick auf das `button.link` der ersten Zeile | `standard` |
|
||||
| `dlg-projekt-archiv` | Reiter „Archiviert“, Klick auf das erste `button.link` | `standard` |
|
||||
| `dlg-projekt-art` | wie in P0 (Art-Frage in `TicketProjects`) | `standard` |
|
||||
| `zuordnen`, `zuordnen-kunde`, `zuordnen-ticket`, `zuordnen-fertig` | `/projects/zuordnen/<id>` eines Projekts ohne `kind`: Schritt 1 (Art), nach Art-Wahl und „Weiter“ Kunde ( „Ja, für einen Kunden“ ), nach Kundenwahl Ticket, am Ende Zusammenfassung | `standard` |
|
||||
| `services` | `/services` | `standard` |
|
||||
| `analyse-zustimmung` | wie in P0 (Ticket 100131, „Einschalten“ in der Analyse-Zeile) | `standard` |
|
||||
| `ticket-projekte` | `/tickets/100131`, Klick auf „Preview ▾“ | `standard` |
|
||||
| `kunde-projects` | `/customers/<id>?tab=projects` (Ansicht aus P4, unverändert, kommt nur ins Gate) | `standard` |
|
||||
|
||||
Der Zugang (`CopyableCredential`, Abschnitt „Portal-Zugang“) steht auf der Übersicht. Dafür entsteht keine zweite Ansicht für dieselbe URL. Die `steps` von `kunde-overview` (P4) werden am Ende ergänzt: Klick auf „Anzeigen“ beim Passwort in `.credential`. Erwartung: „Verbergen“ und „Kopieren“ sind sichtbar, kein `svg` in `.credential`. Danach Klick auf „Verbergen“, damit das Bild unverändert bleibt.
|
||||
|
||||
- [ ] **Step 3: Gate eintragen**
|
||||
|
||||
In `gates.mjs`, nach dem Schema der Übersicht:
|
||||
|
||||
```js
|
||||
GATES.p5 = {
|
||||
...GATES.p4,
|
||||
scoped: {
|
||||
...GATES.p4.scoped,
|
||||
'icon-svg': {
|
||||
views: [...GATES.p4.scoped['icon-svg'].views, 'projekte', 'projekte-*', 'fehler-projekte', 'dlg-projekt-*', 'zuordnen', 'zuordnen-*', 'services', 'analyse-zustimmung', 'ticket-projekte', 'kunde-projects'],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`checks` und `views: '*'` kommen von `p4`. `step-failed`, `external-request` und `console-error` sind in jedem Gate aktiv. Der neue `icon-svg`-Eintrag hat kein `ignore`: Die Ausnahmen `.ticket-projects` (P2) und `.credential` (P4) entfallen, weil `TicketProjects` und `CopyableCredential` in diesem Paket umgebaut werden.
|
||||
|
||||
- [ ] **Step 4: Ausgangslauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t4" --gate p5; echo "exit $?"`
|
||||
Expected: `exit 1`, `icon-svg` auf allen Ansichten mit Projektbausteinen. Einige Schritte finden ihre Knöpfe („Neues Projekt“, „Aktualisieren“, „Anzeigen“) noch nicht. Das ist der Ausgangszustand.
|
||||
|
||||
### Task 5: CSS-Block „Projekte (P5)“
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css` (neuer Block „Projekte (P5)“ am Ende, vor dem Bereichs-CSS der Finanzen, falls dieses dort steht)
|
||||
- Modify: `src/styles/global.css` (alte Regeln `.ticket-projects`, `.ticket-projects-assign`, `.ticket-projects-assign .form-control` löschen, L596-599)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tokens und Klassen aus P1 (`.table`, `.link`, `.badge`, `.list-row`, `.seg`, `.check`, `.cell-title`, `.cell-sub`, `.hide-md`, `.hide-sm`).
|
||||
- Produces: die Klassen der Tabellen unten. Die Komponenten-Tasks benutzen sie wörtlich.
|
||||
|
||||
- [ ] **Step 1: Vorhandenes prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "button\.link|^\.link" src/styles/global.css`
|
||||
Expected: Das Zurücksetzen für `button.link` fehlt oder steht schon. Es wird nur ergänzt, was fehlt.
|
||||
|
||||
- [ ] **Step 2: Block anlegen**
|
||||
|
||||
Die Werte der Projekttabelle sind in einer statischen Probe bei 390, 861, 900, 1101, 1180, 1280 und 1440 px gemessen. Es gibt dabei keinen waagerechten Überlauf. Der Block steht am Ende von `global.css`. Farben kommen nur über Tokens, Abstände zwischen Abschnitten regelt `.page section + section` (P1).
|
||||
|
||||
Gemessene und von Ansichten geprüfte Regeln:
|
||||
|
||||
| Selektor | Regel |
|
||||
|---|---|
|
||||
| `button.link` (falls fehlend) | `background: none; border: 0; padding: 0; font: inherit; text-align: left; cursor: pointer` |
|
||||
| `.projects-table td:first-child` | `min-width: 140px` (geprüft von `projekte-861` und `projekte-1101`) |
|
||||
| `.projects-table th, .projects-table td` | `padding-right: 8px`, in der letzten Zelle `0` |
|
||||
| `.projects-table .cell-title .link, .projects-table .cell-sub` | `overflow-wrap: anywhere` |
|
||||
| `.projects-table .select` | `width: clamp(104px, 10vw, 180px); max-width: 100%` |
|
||||
| `.projects-table td.cell-address code` | `display: block; max-width: 84px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px` |
|
||||
| `.projects-table td.cell-updated` | `min-width: 70px` |
|
||||
| `@media (max-width: 860px)` für `.projects-table` | Die Zeile wird ein Grid mit zwei Spalten (`1fr 1fr`, Abstand `8px 12px`, `padding: 14px 0`, Trennlinie `1px solid var(--line)`). `thead` ist nur für Screenreader sichtbar. `table`, `tbody` und `td` sind `block`, `td` ohne Innenabstand und Rahmen. Erste Zelle und `td.cell-actions` gehen über beide Spalten. `td[data-label]::before` zeigt `attr(data-label)` (12px, `--text-2`). `.select` ist 100 % breit, `.row-actions` linksbündig, kein Zeilen-Hover. `.projects-table td.hide-sm, .projects-table td.hide-md { display: none }` (0,2,1) schlägt `.hide-sm` (0,1,0) und `.projects-table td { display: block }` (0,1,1) über die Spezifität, ohne `!important`. |
|
||||
| `.badge.wrap` | `white-space: normal; height: auto; min-height: 20px; padding-top: 2px; padding-bottom: 2px; overflow-wrap: anywhere` |
|
||||
| `.pv-log` | helle Fläche (`background: var(--bg-subtle)`), `max-height: 260px; overflow: auto; white-space: pre-wrap; word-break: break-all`, Monospace 11px/16px |
|
||||
| `.pv-var code`, `.pv-var .input`, `.pv-add .input` | `flex: 1 1 140px; min-width: 0`, bei `code` zusätzlich `overflow-wrap: anywhere` |
|
||||
|
||||
Übrige Klassen (nur Aufbau, kein Messwert):
|
||||
|
||||
| Klassen | Aufbau |
|
||||
|---|---|
|
||||
| `.projects-table tr.is-archived td` | `opacity: .72` (wie die Karten heute) |
|
||||
| `.row-actions` | Flex-Reihe mit Umbruch (Abstand `4px 14px`, 13px), in der Tabelle rechtsbündig, `.link` ohne Zeilenumbruch |
|
||||
| `.dialog-form` | Flex-Spalte, Abstand 16px |
|
||||
| `.pv-block` | 13px, Flex-Spalte, Abstand 10px |
|
||||
| `.pv-row`, `.pv-add`, `.pv-var` | Flex-Reihe mit Umbruch, Abstand 8px, mittig ausgerichtet. `.pv-var` hat unten `1px solid var(--line)`. |
|
||||
| `.pv-mode`, `.pv-title` | `width: 200px; max-width: 100%` bzw. 13px, `font-weight: 600` |
|
||||
| `.proj-item`, `.proj-head`, `.proj-main`, `.proj-meta` | Zeile mit `padding: 16px 0` und Trennlinie. Kopf als Flex mit Umbruch und `justify-content: space-between`, `.proj-main { min-width: 0; overflow-wrap: anywhere }`. Meta-Zeile 12px, `--text-2`, mit Umbruch. |
|
||||
| `.ticket-projects-assign` | Flex-Reihe mit Umbruch, Abstand 8px, `margin-top: 12px`. `.select` mit `flex: 1 1 220px`, `.input` mit `flex: 1 1 160px`, beide `min-width: 0`. |
|
||||
| `button.list-row` | Knopf-Reset (`width: 100%; text-align: left; background: none; border: 0; border-bottom: 1px solid var(--line); font: inherit; color: inherit; cursor: pointer`). `:hover` und `[aria-pressed="true"]` mit `background: var(--bg-hover)`, `.cell-sub` als `block`. |
|
||||
| `.pick-list`, `.pick-empty` | `max-height: 360px; overflow-y: auto` bzw. `padding: 12px 0` |
|
||||
| `.seg.wrap`, `.row-buttons`, `.check.top` | `flex-wrap: wrap`, Flex mit Umbruch und Abstand 8px, `align-items: flex-start` |
|
||||
| `.summary` | `list-style: none`, Flex-Spalte, Abstand 6px |
|
||||
|
||||
- [ ] **Step 3: Bauen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
|
||||
Expected: Exit 0.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css
|
||||
git commit -m "Projekte: CSS-Block fuer Tabelle, Dialog-Inhalte, Preview-Block und Zuordnen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Link-Bausteine (`PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential`)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/PreviewLinkButton.jsx` (alles ab L7), `src/components/GiteaLinkButton.jsx`, `src/components/CopyableCredential.jsx` (L93-152)
|
||||
- Modify: `src/pages/AdminPage.css` (L330-345: `.credential-action`, `.credential-action:hover`, `.credential-check` löschen, `.credential` und `.credential .link` anpassen)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `.link` aus P1, `createPreviewLink` aus `lib/teamApi`, `openInNewTab` aus `lib/openInNewTab`.
|
||||
- Produces:
|
||||
- `PreviewLinkButton({ href, subdomain, label = 'Vorschau öffnen' })` rendert `<a className="link" href target="_blank" rel="noopener noreferrer" onClick>{label}</a>`. Ohne `href` kommt `null`. Die Logik (Einmal-Link, `window.alert` bei Fehler) bleibt.
|
||||
- `GiteaLinkButton({ href, label = 'In Gitea öffnen' })` rendert `<a className="link" href target rel>{label}</a>`.
|
||||
- `CopyableCredential({ value, secret = false, label = 'Wert' })` (Signatur unverändert) mit Textknöpfen.
|
||||
- Für P8: Von `.credential*` bleiben `.credential`, `.credential-value`, `.credential-value.is-masked`, `.credential-empty` und `.credential .link`. P8 zieht sie nach `global.css`.
|
||||
|
||||
- [ ] **Step 1: Prüfen, dass `title` und `size` nirgends übergeben werden**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "<(PreviewLinkButton|GiteaLinkButton)[^>]*(title|size)=" src`
|
||||
Expected: keine Treffer. Dann entfallen die Props `title` und `size`.
|
||||
|
||||
- [ ] **Step 2: Umbau**
|
||||
- Die Link-Bausteine verlieren Inline-Stil und Icon. Der Text ist `label`.
|
||||
- `CopyableCredential`:
|
||||
- Der Umschalter ist `<button type="button" className="link">` mit „Anzeigen“ bzw. „Verbergen“.
|
||||
- Der Kopierknopf ist `<button type="button" className="link">` mit „Kopieren“ bzw. nach dem Klick „Kopiert“.
|
||||
- `aria-label` und `title` führen die sichtbaren Wörter mit: `${label} anzeigen`, `${label} verbergen`, `${label} kopieren`, `${label} kopiert`.
|
||||
- Maskierung, Timer, `stopPropagation` und `copyText` bleiben.
|
||||
- Entscheidung: `aria-label` und `title` sagen „verbergen“ statt „ausblenden“, damit der zugängliche Name das sichtbare Wort enthält.
|
||||
- `.credential`: `gap: 8px`, `.credential .link { flex: 0 0 auto; font-size: 12px; white-space: nowrap }`. `AdminPage.css` lädt vor `global.css`. `.credential .link` (0,2,0) schlägt `.link` und `button.link` (0,1,1) über die Spezifität und braucht kein `!important`.
|
||||
|
||||
- [ ] **Step 3: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t6" --only kunde-overview,ticket-projekte --gate p5; echo "exit $?"`
|
||||
Expected: `kunde-overview` besteht (Login und Passwort mit Textknöpfen, kein `svg` in `.credential`, auch nach „Anzeigen“). `ticket-projekte` darf noch Verstöße der späteren Tasks zeigen, aber „Vorschau öffnen“ und „In Gitea öffnen“ stehen als Text da.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/CopyableCredential.jsx src/pages/AdminPage.css
|
||||
git commit -m "Projekte: Vorschau-, Gitea- und Zugangsknoepfe als Textlinks" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: `PreviewBlock`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/PreviewBlock.jsx` (Import L1-12 behalten, `dotColor`-Import und `STATE_OF_COLOR` L14 löschen, Markup L174-297 neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Button`, `Badge` aus `components/ui`, `previewColorTone` aus `./projectTones`, `previewStatus`, `modeOptionsFor`, `modeHint`, `isDevUnavailableError`, `NO_DEV_HINT` aus `lib/previewView`, Klassen `.pv-*`, `.input`, `.select`, `.msg`, `.link`, `.badge.wrap`.
|
||||
- Produces: `PreviewBlock({ projectId })` (Signatur unverändert).
|
||||
|
||||
- [ ] **Step 1: Umbau**
|
||||
|
||||
Alle Zustände, Handler, Intervalle und Texte bleiben. Nur das Markup ändert sich.
|
||||
|
||||
| Heute | Danach |
|
||||
|---|---|
|
||||
| „Lädt …“ | `<p className="muted xs">Lädt …</p>` |
|
||||
| Label „Modus“ und Auswahl | `.pv-row`: `<label className="muted xs" htmlFor>`, `<select className="select pv-mode">` |
|
||||
| `NO_DEV_HINT`, `modeHint` | `<p className="muted xs">` |
|
||||
| Punkt und Statustext | `<Badge tone={previewColorTone(status.color)} className="wrap">{status.text}</Badge>` |
|
||||
| Fehlertext des Builds, `error`, `envError` | `<p className="msg err">`. `error` zusätzlich mit `role="alert"`. |
|
||||
| „Preview-Runner ist nicht aktiv.“ | `<p className="muted xs">` |
|
||||
| Neu bauen, Protokoll ansehen, Jetzt starten, Stoppen | `Button` (Standard), `type="button"`, in `.pv-row`, `disabled`-Bedingungen unverändert |
|
||||
| Lauf-Auswahl und `<pre>` | `<select className="select">` (`aria-label="Lauf wählen"`) und `<pre className="pv-log">` |
|
||||
| `<strong>Variablen</strong>` | `<h4 className="pv-title">Variablen</h4>` |
|
||||
| Variablenzeile | `.pv-var`: `<code>`, „•••“, bei Bearbeitung `input.input` und `<button type="button" className="link">Übernehmen</button>`, sonst `<button type="button" className="link">Ändern</button>`, dazu `<button type="button" className="link">Entfernen</button>` |
|
||||
| Neue Variable | `.pv-add`: zwei `input.input` (Enter-Handler bleiben) und `Button type="button"` „Hinzufügen“ |
|
||||
| „Variablen speichern“ | `Button` (Standard), `type="button"` |
|
||||
| Wurzel | `<div className="pv-block">` |
|
||||
|
||||
- [ ] **Step 2: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t7" --only ticket-projekte --gate p5; echo "exit $?"`
|
||||
Expected: Im Bild steht der geöffnete Preview-Block ohne Knopfrahmen-Reihe, ohne dunklen Log-Kasten. `icon-svg` an der Projektzeile und am Abschnitt (Tasks 9 und 10) darf noch melden.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "btn-sm|#[0-9a-f]{3,6}|style=\{" src/components/PreviewBlock.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/PreviewBlock.jsx
|
||||
git commit -m "Projekte: Preview-Block ohne Kleinknoepfe, Statuspunkt als Badge, Log auf hellem Grund" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: `AnalyseRow` (Zustimmung als Dialog)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/AnalyseRow.jsx` (`COLORS` L5 löschen, Markup L58-132 neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Dialog`, `Button`, `Badge`, `Field` aus `components/ui` (P1), `analyseColorTone` aus `./projectTones`, alle Funktionen aus `lib/analyseView` unverändert (`analyseStatus`, `textformLabel`, `canSubmitTextform`, `channelLabel`, `legalLinks`, `chatTemplate`, `privacyHint`).
|
||||
- Produces: `AnalyseRow({ projectId })` (Signatur unverändert).
|
||||
|
||||
- [ ] **Step 1: Umbau**
|
||||
|
||||
Zustände, `apply`, `cancel`, `copyTemplate`, `canSubmit` und alle Wortlaute bleiben. Neu ist das Markup:
|
||||
|
||||
| Heute | Danach |
|
||||
|---|---|
|
||||
| Zeile „Website-Analyse:“ | `<div className="pv-row">`: `<span className="muted">Website-Analyse:</span>`, dann Status |
|
||||
| Status | `data`: `<Badge tone={analyseColorTone(status.color)} className="wrap">{status.text}</Badge>`; `loadError`: `<span className="msg err" role="alert">`; sonst `<span className="muted">Lädt …</span>`; `status.hint` als `<span className="muted">({status.hint})</span>` |
|
||||
| „Einschalten“ (oder `status.enableLabel`) und „Ausschalten“ | `<button type="button" className="link">` mit dem Text von heute |
|
||||
| Zustimmungsablauf (`mode === 'on'`) | `<Dialog title="Website-Analyse" onClose={cancel} busy={busy} footer={…}>` |
|
||||
| Kästchen mit `textformLabel` | `<label className="check top">`. Die beiden Links sind `<a className="link" target="_blank" rel="noopener noreferrer">`, Wortlaut aus `label.before/avv/between/lb/after`. |
|
||||
| „Datum der Zustimmung:“ | `<Field label="Datum der Zustimmung:" htmlFor>` mit `input.input` (`type="date"`, `required`, `min`, `max` wie heute) |
|
||||
| „Weg:“ und die zwei Radios | `<div className="field" role="radiogroup" aria-label="Weg der Zustimmung">`, `<span className="strong">Weg:</span>`, Radios in `<label className="check">` mit `channelLabel(c)` |
|
||||
| `privacyHint` | `<p className="msg err">` |
|
||||
| `<details>` „Textvorlage für den Chat“ | bleibt `<details>`. Die Vorlage steht in `<p className="prose">`, „Kopieren“/„Kopiert“ ist `Button` (Standard) mit `type="button"`. |
|
||||
| „Einschalten“ und „Abbrechen“ | Fuß: `<span className="grow" />`, `Button type="button"` „Abbrechen“ (`disabled={busy}`), `Button type="button" variant="primary"` „Einschalten“ (`disabled={!canSubmit \|\| busy}`) |
|
||||
| Ausschalten-Rückfrage (`mode === 'off'`) | bleibt eine Zeile ohne Dialog: Text „Website-Analyse wirklich ausschalten?“, `<button type="button" className="link-danger">Ja, ausschalten</button>`, `<button type="button" className="link">Abbrechen</button>` |
|
||||
| `error` | im Dialog als `<p className="msg err" role="alert">` ganz oben im Dialog-Inhalt, sonst unter der Zeile |
|
||||
|
||||
- Entscheidung: Der Dialogtitel ist „Website-Analyse“ (das Wort der Zeile, ohne Doppelpunkt). Ein Titel muss sein, ein neuer Wortlaut ist nicht nötig.
|
||||
- Entscheidung: Die Ausschalten-Rückfrage bleibt inline, nur die Zustimmung ist ein Dialog (wie in der Spec).
|
||||
- Entscheidung: Die Reihenfolge im Fuß folgt dem Prototyp (Abbrechen links vom Hauptknopf).
|
||||
|
||||
- [ ] **Step 2: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t8" --only analyse-zustimmung --gate p5; echo "exit $?"`
|
||||
Expected: Der Dialog hat keinen Verstoß. `icon-svg` darf noch an `AssignedProjectCard` und `TicketProjects` melden (Tasks 9 und 10). Bei 390 px liegt der Dialog unten als Sheet, „Einschalten“ ist erreichbar. Esc schließt, bei `busy` nicht. Der Text im Kästchen ist unverändert (Stichprobe: Diff gegen `textformLabel`).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/AnalyseRow.jsx
|
||||
git commit -m "Projekte: Website-Analyse-Zeile ohne Farbliterale, Zustimmung als Dialog (Wortlaut unveraendert)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 9: `AssignedProjectCard`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/AssignedProjectCard.jsx` (Icon-Imports und `STATUS_COLORS`/`StatusBadge` L2-35 ersetzen, Markup L81-176 neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Button`, `Badge` aus `components/ui`, `statusValueTone` aus `./projectTones`, `PreviewLinkButton`, `GiteaLinkButton`, `PreviewBlock`, `AnalyseRow`, Klassen `.proj-*`, `.row-actions`, `.link`, `.msg`.
|
||||
- Produces: `AssignedProjectCard({ project, onUnassign, showActions = false, customerName = '', onChanged = () => {} })` (Signatur unverändert). Aufrufer sind `TicketProjects` und der Projekte-Reiter der Kundenseite (P4).
|
||||
|
||||
- [ ] **Step 1: Umbau**
|
||||
|
||||
Handler (`openPreviewInput`, `handleEnablePreview`, `handleBackfill`) und Zustände bleiben.
|
||||
|
||||
| Heute | Danach |
|
||||
|---|---|
|
||||
| Karte mit blauem Rahmen | `<div className="proj-item">` |
|
||||
| Name, `(subdomain)`, Repo mit `FaCodeBranch` | `.proj-head > .proj-main`: `<strong>`, `<span className="muted small">`, `<div className="cell-sub">{repoFullName}</div>` |
|
||||
| Vorschau-, Gitea-Knopf, Lösen-Knopf | `.row-actions` mit `PreviewLinkButton`, `GiteaLinkButton` und `<Button variant="danger" type="button" title="Vom Ticket lösen" aria-label="Vom Ticket lösen">Lösen</Button>`. Er erscheint unter derselben Bedingung (`showActions && onUnassign`). |
|
||||
| `StatusBadge` mit Punkt | `.proj-meta`: `<span>Status: <Badge tone={statusValueTone(value)}>{value}</Badge></span>`, ebenso „Provisioning:“. Ohne Wert nichts. |
|
||||
| `AnalyseRow` | unverändert eingebunden (`project.subdomain`) |
|
||||
| „Preview ▴/▾“ (`btn btn-sm`) | `<button type="button" className="link" aria-expanded>` mit demselben Text. Die Zeichen ▴ und ▾ bleiben, sie sind Text. Der `PreviewBlock` steht darunter. |
|
||||
| „Preview einschalten“, „Einschalten“, „Pushes nachtragen“ | `.row-actions` (linksbündig): zwei `<button type="button" className="link">`, `input.input` mit `Button type="button"` „Einschalten“ (`disabled={!subdomain \|\| previewBusy}`), `<button type="button" className="link" disabled={pushBusy}>Pushes nachtragen</button>` |
|
||||
| Meldungen | `<p className="msg muted">` je `previewMessage`, `pushMessage` |
|
||||
|
||||
- [ ] **Step 2: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t9" --only ticket-projekte,kunde-projects --gate p5; echo "exit $?"`
|
||||
Expected: `kunde-projects` besteht. `ticket-projekte` darf noch `icon-svg` am Abschnitt melden (Task 10). Die Projektzeile am Ticket zeigt Name, Repo, drei Textlinks, Status-Badges und die Analyse-Zeile ohne Kasten.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|btn-sm|#[0-9a-f]{3,6}|style=\{" src/components/AssignedProjectCard.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/AssignedProjectCard.jsx
|
||||
git commit -m "Projekte: Projektzeile am Ticket und beim Kunden ohne Kasten, Aktionen als Textlinks" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 10: `TicketProjects`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/TicketProjects.jsx` (Import L2 löschen, Markup L158-228 neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Card`, `Button`, `Dialog` aus `components/ui`, `AssignedProjectCard`, Klassen `.ticket-projects-assign`, `.row-buttons`, `.msg`.
|
||||
- Produces: `TicketProjects({ ticket })` (Signatur unverändert, P2 bindet sie auf der Ticketseite ein). `data-testid="project-kind"` bleibt.
|
||||
|
||||
- [ ] **Step 1: Umbau**
|
||||
|
||||
Alle Zustände und Handler (`run`, `assign`, `assignPlain`, `assignWithKind`, `unassign`, `create`) bleiben.
|
||||
|
||||
| Heute | Danach |
|
||||
|---|---|
|
||||
| `ui-card pad` mit `<h5>` | `<Card title={\`Projekte (${assigned.length})\`}>` |
|
||||
| „Suche neue Repos …“ | `<p className="muted">` |
|
||||
| `importHint`, `error` | `<p className="msg err" role="alert">` |
|
||||
| „Noch kein Projekt an diesem Ticket.“ | `<p className="muted">` |
|
||||
| Art-Zeile und Karte je Projekt | `<span className="muted xs" data-testid="project-kind">` und `AssignedProjectCard` (Props wie heute) |
|
||||
| Auswahl „Projekt zuweisen“ | `.ticket-projects-assign`: `<select className="select" aria-label="Projekt zuweisen">`, Optionstexte wie heute |
|
||||
| „Zuweisen“ (`FaLink`) | `Button type="button"` (Standard), `disabled={!selected \|\| busy}` |
|
||||
| „Neues Projekt“ (`FaPlus`) | `Button variant="quiet" type="button"`, schaltet das Formular um |
|
||||
| `assignResult` | `<p className="msg">` mit den Warnungen als `<span className="muted">` |
|
||||
| Anlegen-Formular | bleibt inline: `<form className="ticket-projects-assign" onSubmit>` mit zwei `input.input` (`required`) und `Button type="submit" variant="primary"` („Anlegen & zuweisen“, beim Laden „Wird angelegt …“) |
|
||||
| „Was ist das?“-Overlay | `<Dialog title="Was ist das?" onClose={() => setKindDialog(false)}>`. Darin „Bitte wähle die Art des Projekts, bevor es zugewiesen wird.“ als `<p className="muted">`, der Fehler „Arten konnten nicht geladen werden – bitte neu laden.“ als `msg err`, die Arten als `.row-buttons` mit `Button type="button"` je `k.label` (`onClick={() => assignWithKind(k.key)}`). |
|
||||
|
||||
Danach die toten Klassen prüfen:
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticket-projects[^-]|ui-btn|ui-card" src/components/TicketProjects.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 2: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t10" --only ticket-projekte,dlg-projekt-art --gate p5`
|
||||
Expected: PASS. Bei 390 px ist der Dialog ein Sheet. Esc schließt ihn. Die Auswahl „Projekt zuweisen“ läuft nicht über den Rand.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/TicketProjects.jsx
|
||||
git commit -m "Projekte: Projektabschnitt am Ticket als Abschnitt, Art-Frage als Dialog" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 11: `ProjectsPage`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/ProjectsPage.jsx`
|
||||
- L1-85 (Importe, `TYPES`, `typeOf`, `HOSTED_TYPES`, `isProjectReady`) durch Importe aus `lib/projectRow` ersetzen
|
||||
- L358-369 (`FILTER_OPTIONS`, `typeOptions`) ersetzen
|
||||
- L371-782 (Markup) neu
|
||||
- `Row` (L784-791) und die tote `customerMap` (L131-137) löschen
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes:
|
||||
- aus Task 1 bis 3: `PROJECT_TYPES`, `projectActions`, `projectTicketRef`, `updatedLabel`, `typeOf`, `projectTabs`
|
||||
- aus P1: `PageHeader`, `Tabs`, `Button`, `Badge`, `EmptyState`, `Field`, `Dialog`
|
||||
- aus P2: `ticketPath` und `isRowActivation(event)` aus `src/lib/rowClick.js`
|
||||
- Hooks unverändert (`useWebsiteProjects`, `useCustomers`, `useWorkorders`, `useEmployees`)
|
||||
- Klassen `.projects-table`, `.row-actions`, `.dialog-form`, `.pv-title`
|
||||
- Produces: Seite `/projects`. Die Route `/projects/zuordnen/:id` wird nach „Projekt anlegen“ wie heute aufgerufen.
|
||||
|
||||
- [ ] **Step 1: Zustand und Laden**
|
||||
- Neu: `const [loadedOnce, setLoadedOnce] = useState(false)`. Im Effekt: `fetchAllProjects().then(() => { if (!cancelled) setLoadedOnce(true) })`. Der Effekt behält sonst Reihenfolge und Import-Aufruf.
|
||||
- Neu: Der Text „Wird geladen …“ (`<div className="loading">`) erscheint nur, solange `!loadedOnce`. Danach bleibt die Tabelle bei jedem Nachladen stehen, auch wenn `useWebsiteProjects` `loading` setzt. Der Hook bleibt unverändert.
|
||||
- Entscheidung: Die Wurzel ist `<div className="page">` (seit P1), der Titel „Website-Projekte“ ohne Untertitel (ein neuer Untertitel wäre neuer Text).
|
||||
|
||||
- [ ] **Step 2: Kopf, Reiter, Filterzeile, Meldungen**
|
||||
|
||||
| Heute | Danach |
|
||||
|---|---|
|
||||
| `<h2>Website-Projekte</h2>` | `PageHeader title="Website-Projekte"` |
|
||||
| Knopf „+“ (Titel „Neues Projekt“) | `Button variant="primary" type="button"` „Neues Projekt“ in `actions`, öffnet den Dialog |
|
||||
| Knopf Sync (Titel „Gitea-Repos synchronisieren“, Spinner) | `Button type="button"` „Gitea-Repos synchronisieren“ in `actions`, `disabled={syncLoading}`, beim Laden Text „Wird geladen …“ |
|
||||
| Ansicht-Select mit sechs Optionen | `Tabs tabs={projectTabs(counts, { isAdmin })} active={filter} onChange={setFilter}`. „Vorlagen“ nur für Admins. |
|
||||
| Suchfeld | `input.input.search type="search"`, Platzhalter „Nach Titel suchen…“, in `.toolbar`, `aria-label="Nach Titel suchen"` |
|
||||
| Kunde-Filter, Mitarbeiter-Filter | zwei `select.select.select-quiet` in `.toolbar`, Titel und Optionen wie heute |
|
||||
| `notice` | `<p className="msg muted" role="status">` |
|
||||
| „Weitere Treffer“ | `<p className="msg muted" data-hidden-hits>`: „Weitere Treffer: “, je Ansicht `<button type="button" className="link">{h.count} unter „{VIEW_LABELS[h.view]}“</button>`, getrennt durch „ · “. Klick setzt `setFilter(h.view)`. |
|
||||
| `error` | `<p className="msg err" role="alert">` |
|
||||
| Leere Liste | `EmptyState title` mit „Keine archivierten Projekte.“ bzw. „Keine Projekte gefunden.“ |
|
||||
|
||||
- [ ] **Step 3: Tabelle**
|
||||
|
||||
`<table className="table projects-table" role="table">`, Zeilen `role="row"`, Zellen `role="cell"` (Tabellensemantik bleibt beim Stapeln unter 860 px erhalten). `<tr className={\`row${archived ? ' is-archived' : ''}\`}>` mit `onClick={(e) => { if (isRowActivation(e)) openEdit(project) }}`. `isRowActivation` ignoriert Klicks auf `a, button, select, input, label`, Mehrfach-Modifikatoren und andere Maustasten.
|
||||
|
||||
| Spalte (Kopf) | Klassen | Inhalt |
|
||||
|---|---|---|
|
||||
| Projekt | – | `.cell-title` mit `<button type="button" className="link" onClick={() => openEdit(project)}>{projectName \|\| giteaRepoName \|\| '(ohne Name)'}</button>`, darunter `.cell-sub` `repoFullName` |
|
||||
| Kunde | `data-label="Kunde"` | `select.select` mit Optionen „Kein Kunde“ und `({code}) {name}`, `aria-label={\`Kunde von ${name}\`}`, `disabled={assignBusyId === id \|\| archived}`, `onChange` ruft `handleAssignCustomer` (mit `window.confirm` beim Entfernen, unverändert) |
|
||||
| Art | `hide-md` | `<Badge tone={PROJECT_TYPES[type].tone}>{short}</Badge>` |
|
||||
| Adresse | `hide-md cell-address` | `<code title={subdomain}>{subdomain}</code>`, sonst `<span className="faint">ohne Website</span>` |
|
||||
| Ticket | `hide-sm num` | `<Link className="link" to={ticketPath(projectTicketRef(project, ticketMap))}>`, Text derselbe Wert. Ohne Ticket `<span className="faint">-</span>`. |
|
||||
| Status | `data-label="Status"` | `hostedBadge` als `Badge` (nur wenn nicht `null`), darunter `.cell-sub` mit `archived ? 'archiviert' : (status \|\| '-')` |
|
||||
| Zuletzt geändert | `hide-md right cell-updated` | `updatedLabel(project.$updatedAt).text`, `title` aus `.title` |
|
||||
| Aktionen | `right cell-actions` | `.row-actions` mit den vier Links unten |
|
||||
|
||||
Aktionen, jeweils nur wenn zutreffend (`projectActions(project)`):
|
||||
|
||||
| Bedingung | Link |
|
||||
|---|---|
|
||||
| `host` | `<button type="button" className="link" title="Projekt jetzt deployen/hosten" disabled={hostBusyId === id}>` „Hosten“, beim Laden „Wird geladen …“ |
|
||||
| `preview` | `PreviewLinkButton` „Vorschau öffnen“ |
|
||||
| `refresh` | `<button type="button" className="link" title="Aktualisieren – Website neu deployen" disabled={refreshBusyId === id}>` „Aktualisieren“, beim Laden „Wird geladen …“ |
|
||||
| `gitea` | `GiteaLinkButton` „In Gitea öffnen“ |
|
||||
|
||||
- Entscheidung: Kein Tabellenfuß (die Zähler stehen in den Reitern).
|
||||
- Entscheidung: Leere Werte bleiben „-“ wie heute, nicht „–“.
|
||||
- Entscheidung: „Adresse“ zeigt wie die Karte heute die Subdomain.
|
||||
- Entscheidung: Das Wort „Hosten“ erhält als Ladezustand den Text „Wird geladen …“ aus der Spec-Liste.
|
||||
- Entscheidung: Das alte „Bearbeiten“ (Stift) entfällt als eigener Knopf. Sein Platz ist das `button.link` der ersten Zelle und der Zeilenklick.
|
||||
- Entscheidung: Unter 860 px stapelt die Zeile (Projekt, dann Kunde und Status nebeneinander, dann Aktionen), weil Auswahl und Aktionen sonst keinen Platz finden. Die Kernspalten laut Spec bleiben dabei Projekt, Kunde, Status und Aktionen.
|
||||
|
||||
- [ ] **Step 4: Dialog „Neues Projekt“**
|
||||
|
||||
`Dialog as="form" onSubmit={handleCreate} title="Neues Website-Projekt" busy={createLoading} onClose={() => setShowCreateModal(false)}`. Fuß: `<span className="grow" />` und `Button type="submit" variant="primary"` („Projekt anlegen“, beim Laden „Wird angelegt...“).
|
||||
|
||||
Inhalt (Texte unverändert, `Field` mit `htmlFor`):
|
||||
- `createError` als `<p className="msg err" role="alert">` ganz oben
|
||||
- „Anzeigename“ (`required`)
|
||||
- „Subdomain (optional)“ mit Platzhalter und dem Hinweis „Mit Subdomain wird die Website … als normales Projekt angelegt.“ als `hint`
|
||||
- „Repo-Name (optional)“ mit Platzhalter
|
||||
- „Kunde (optional)“
|
||||
|
||||
Der Kommentar zum Repo-Namen (nicht aus der Subdomain mitfüllen) bleibt.
|
||||
|
||||
- [ ] **Step 5: Dialog „Projekt bearbeiten“**
|
||||
|
||||
`Dialog wide title="Projekt bearbeiten" busy={editLoading \|\| archiveBusy} onClose={() => setEditProject(null)}`. Die Box ist ein `div`, damit der `PreviewBlock` außerhalb des Formulars steht.
|
||||
- Inhalt in `.dialog-body`:
|
||||
- `<p className="muted small">{editProject.repoFullName}</p>`
|
||||
- `editError` als `msg err`
|
||||
- `<form id="projekt-bearbeiten" className="dialog-form" onSubmit={handleEditSave}>` mit den Feldern „Name“ (`required`), „Typ / Template“ (Optionen aus `PROJECT_TYPES`, `adminOnly` nur für Admins, `hint` ohne Icon) und, außer bei Typ `project`, „Subdomain“ (Hinweis „Ergebnis: https://…project.webklar.com - Aenderung wird neu deployt (alte Subdomain wird abgebaut).“)
|
||||
- bei archiviertem Projekt `<p className="muted small">Archiviert: zum Bearbeiten oder Hosten erst reaktivieren.</p>`
|
||||
- unter denselben Bedingungen wie heute (`projectType !== 'project' && subdomain && !archived`) `<section><h4 className="pv-title">Preview</h4><PreviewBlock key projectId /></section>`
|
||||
- Fuß:
|
||||
- links `Button variant="danger" type="button"` „Repository archivieren“ (`window.confirm` bleibt) bzw. bei archivierten Projekten `<button type="button" className="link">Reaktivieren</button>`, `disabled={archiveBusy \|\| editLoading}`
|
||||
- dann `<span className="grow" />`
|
||||
- rechts `Button type="submit" form="projekt-bearbeiten" variant="primary"` („Speichern & synchronisieren“, beim Laden „Speichert...“, `disabled` bei `editLoading \|\| archiveBusy \|\| archived`, `title` wie heute)
|
||||
- Entscheidung: Der Speichern-Knopf steht im Fuß außerhalb des `<form>` und hängt über das Attribut `form` daran. So löst Enter im Namensfeld Speichern aus, im `PreviewBlock` nie.
|
||||
|
||||
- [ ] **Step 6: Alles Übrige löschen**
|
||||
|
||||
Entfallen: Icon-Importe, `TYPES`, `typeOf`, `HOSTED_TYPES`, `isProjectReady` (jetzt in `lib/projectRow`), `Row`, `customerMap`, `FILTER_OPTIONS`. Die Handler `handleCreate`, `handleSyncRepos`, `handleAssignCustomer`, `handleHostNow`, `handleRefresh`, `openEdit`, `handleEditSave`, `handleArchiveToggle` bleiben bis auf den Ersatz von `typeOf` durch den Import unverändert.
|
||||
|
||||
- [ ] **Step 7: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t11" --only projekte,projekte-archiv,projekte-treffer,projekte-lang,projekte-viel,projekte-861,projekte-1101,projekte-aktion,projekte-tastatur,projekte-zeilenklick,fehler-projekte,dlg-projekt-neu,dlg-projekt-bearbeiten,dlg-projekt-archiv --gate p5`
|
||||
Expected: PASS (Exit 0).
|
||||
- Kein `overflow-x`, auch bei 861 und 1101 px, die erste Zelle ist dort (gerundet) mindestens 140 px breit.
|
||||
- Bei 390 px stehen Projekt, Kunde, Status und Aktionen gestapelt in der Zeile.
|
||||
- `projekte-aktion` und `projekte-tastatur` bestehen.
|
||||
- Im Bild `projekte` steht höchstens ein schwarzer Knopf im Kopf („Neues Projekt“).
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/ProjectsPage.jsx
|
||||
git commit -m "Projekte: Liste als Tabelle mit Reitern, Neu- und Bearbeiten-Dialog, Laden nur beim ersten Mal" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 12: `AssignProjectPage`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/AssignProjectPage.jsx` (Import L3 löschen, Rückgaben ab L178 neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageHeader`, `Card`, `Button`, `Field`, `Badge` aus `components/ui`, `CreateCustomerForm` (Dialog aus P4, Props `onCancel`, `onCreate`), `ticketPath` (P2), Klassen `.seg.wrap`, `.pick-list`, `button.list-row`, `.row-buttons`, `.summary`, `.check`, `.field-row-3`.
|
||||
- Produces: Seite `/projects/zuordnen/:id`. Zustände, Effekte, `finish`, `go`, `patch`, `startNewTicket`, `setNT`, `stepReady`, die Logik aus `lib/assignFlow` und der 409-Fall bleiben unverändert.
|
||||
|
||||
- [ ] **Step 1: Vorher prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath" src/pages/AssignProjectPage.jsx`
|
||||
Expected: 2 Treffer (P2). Sie bleiben bis zum Schluss erhalten.
|
||||
|
||||
- [ ] **Step 2: Kopf und Zustände**
|
||||
- Alle `Button` dieser Seite tragen `type="button"`, die Seite hat kein `<form>`.
|
||||
- Ein gemeinsamer Kopf `PageHeader crumb={[{ label: 'Projekte', to: '/projects' }, { label: repoName \|\| project.projectName }]} title="Projekt zuordnen" subtitle={…}` für alle Zustände. `subtitle` ist die Zeile der bisherigen Kopf-Karte: „Repository in Gitea öffnen“ (`<a className="link" target="_blank" rel="noreferrer">`) und „ · angelegt am {Datum}“ mit „ von {createdByGitea}“, Wortlaut wie heute.
|
||||
- Entscheidung: Der bisherige Untertitel mit dem Schrittnamen entfällt. Dieselbe Zeile steht direkt darunter als Abschnittstitel `h2.section` (kommt von `Card title`). `STEP_TITLES` bleibt unverändert.
|
||||
- Laden: `<div className="page"><div className="loading">Wird geladen …</div></div>`. Ladefehler: `PageHeader` mit der Brotkrume nur „Projekte“ (es gibt noch kein `project`) und `<p className="msg err" role="alert">{loadError}</p>`.
|
||||
- Zustand „Zugeordnet, mit Hinweisen“: `<Card title="Zugeordnet, mit Hinweisen">` mit den Warnungen als `<ul className="summary">` und `Button variant="primary"` „Weiter“.
|
||||
- Zustand „Art bereits gesetzt“: Hinweis (`notice` als `msg err`), „Bereits erledigt von **…** am …“ und `TicketLink` (`Link className="link"`, Text „Zum Ticket #{woid}“) ohne Kasten.
|
||||
|
||||
- [ ] **Step 3: Schritte**
|
||||
|
||||
Je Schritt `<Card title={STEP_TITLES[state.step]}>`. „Zurück“ (`Button variant="ghost"`) und „Weiter“ (`Button variant="primary"`) bleiben mit denselben Bedingungen, ohne Pfeil-Icons. Der Fehler (`error`) ist `msg err`.
|
||||
|
||||
| Schritt | Danach |
|
||||
|---|---|
|
||||
| Art | `<div className="seg wrap" role="group" aria-label={STEP_TITLES.kind}>` mit je einem `<button type="button" aria-pressed={state.kind === k.key}>`. Das Verhalten (`patch({ kind, newTicket: null, ticketId: '' })`) bleibt. |
|
||||
| „Ist das für einen Kunden?“ | `.row-buttons`: `Button variant="primary"` „Ja, für einen Kunden“, `Button` „Nein, internes Projekt“ |
|
||||
| Kunde | Zeile aus `input.input.search` (Platzhalter „Kunde suchen …“) und für Admins `Button` „Neuer Kunde“ (ohne `FaPlus`). `showNewCustomer && isAdmin` rendert `CreateCustomerForm` wie P4 es vorsieht, ohne umgebendes `div`. Liste in `.pick-list`, je Kunde `<button type="button" className="list-row" aria-pressed>` mit `.cell-title` (Name, `· code` als `.faint`) und `.cell-sub` (`ort · mail` oder `-`). „Kein Kunde gefunden.“ als `<p className="muted pick-empty">`. |
|
||||
| Ticket | Laden: `<p className="muted">Wird geladen …</p>`. Fehler: `msg err` mit „Die Tickets des Kunden konnten nicht geladen werden.“ und `Button` „Erneut versuchen“. Liste wie beim Kunden mit `#{woid} {topic}` und `type · status`. „Dieser Kunde hat keine offenen Tickets.“ als `pick-empty`. „Neues Ticket anlegen“ als `Button` (primary, solange `newTicket` gesetzt ist, wie heute). Das Formular: `.field-row-3` mit „Thema“, „Angefragt von“, „Start“ (`type="date"`), darunter „Details“ als `textarea.textarea`, dann „Ticket-Art: … (ergibt sich aus der Art)“ als `<p className="muted small">`. |
|
||||
| Zusammenfassung | `<ul className="summary">` mit den Zeilen von heute (Art, Kunde bzw. „Internes Projekt (ohne Kunde)“, Ticket bzw. „Neues Ticket“). Preview-Hinweise als `<p className="muted">`. „Preview einschalten“ als `<label className="check"><input type="checkbox" …>`, „Subdomain“ als `Field` mit dem `hint`. „Abschließen“ als `Button variant="primary"`, beim Laden „Speichert …“. |
|
||||
|
||||
- Entscheidung: Die Auswahl ist nicht nur an Hintergrund und Häkchen erkennbar. Die gewählte Zeile trägt `aria-pressed="true"`, den Hintergrund `--bg-hover` und das Badge „Ausgewählt“ (`<Badge>Ausgewählt</Badge>` am Zeilenende). Das Wort ist neu und von der Spec verlangt („Auswahl auch als Text markiert“).
|
||||
- Entscheidung: Die Zusammenfassung bleibt eine Liste mit dem Wortlaut von heute (kein `.kv`, damit die Doppelpunkte und Sätze unverändert bleiben).
|
||||
- Entscheidung: Der Brotkrumen-Link führt immer auf `/projects`.
|
||||
|
||||
- [ ] **Step 4: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t12" --only zuordnen,zuordnen-kunde,zuordnen-ticket,zuordnen-fertig --gate p5`
|
||||
Expected: PASS. Bei 390 px läuft nichts über den Rand, die gewählte Zeile zeigt „Ausgewählt“.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath" src/pages/AssignProjectPage.jsx`
|
||||
Expected: weiter 2 Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|surface-2|text-red" src/pages/AssignProjectPage.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/AssignProjectPage.jsx
|
||||
git commit -m "Projekte: Zuordnen-Assistent mit Abschnitten, Brotkrumen und im Text markierter Auswahl" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 13: `ServicesPage`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/ServicesPage.jsx` (Importe L2-4 anpassen, Markup L30-84 neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageHeader`, `Card`, `Button`, `Badge` aus `components/ui`, `previewStateTone` aus `components/projectTones`, `servicesCards` unverändert, `ticketPath` (P2, bleibt wie umgestellt), Klassen `.list-row`, `.cell-title`, `.cell-sub`, `.link`, `.msg`.
|
||||
- Produces: Seite `/services`.
|
||||
|
||||
- [ ] **Step 1: Umbau**
|
||||
- Laden: `<div className="loading">Wird geladen …</div>`.
|
||||
- Fehler: `<p className="msg err" role="alert">` und `Button type="button"` „Erneut versuchen“, ohne Kasten.
|
||||
- Das Kachelraster entfällt. Jede Art ist ein untereinander stehender Abschnitt `<Card title={\`${c.label} (${c.count})\`}>`.
|
||||
- Beschreibung als `<p className="muted">`. Funktions-Badges bleiben `<Badge>` mit dem Text `label · statusLabel` in einer Zeile (`.pv-row`).
|
||||
- Projektzeilen sind `.list-row` (kein Link, `div`): links `.cell-title` mit Projektname, dem Badge „Analyse an“ (nur bei `features.analyse === 'active'`) und davor dem Preview-Badge, darunter `.cell-sub` (`Kunde · Preview` oder „Intern“, wie heute). Rechts `Link className="link"` „Ticket #{woid}“ und in der Karte „Art fehlt“ `Link className="link" to={\`/projects/zuordnen/${p.id}\`}` „Zuordnen“.
|
||||
- „Noch keine Projekte.“ als `<p className="muted pick-empty">`.
|
||||
- Preview-Badge: `<Badge tone={previewStateTone(p.previewState)}>Preview: {p.previewState}</Badge>`.
|
||||
- Der Import von `dotColor`, `FaSpinner`, `useNavigate` und der Aufruf `navigate` entfallen.
|
||||
- Entscheidung: Das Badge trägt den Text `Preview: {Zustand}`. Das ist der Wortlaut, den der Punkt heute als Titel und für Screenreader hat. Die Wörter des Anhangs („Online“, „Wird gebaut“, „Fehler“, „Aus“) stehen nicht in der Spec-Liste „Texte“ und kommen deshalb nicht hinein.
|
||||
- Entscheidung: „Zuordnen“ ist ein echter Link statt eines schwarzen Knopfes. Es ist eine Zeilenaktion, und das Ziel ist dasselbe.
|
||||
|
||||
- [ ] **Step 2: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5-t13" --only services --gate p5`
|
||||
Expected: PASS. Bei 390 px laufen Zeilen mit langem Namen nicht über den Rand.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "ticketPath" src/pages/ServicesPage.jsx`
|
||||
Expected: weiter 1 Treffer.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/ServicesPage.jsx
|
||||
git commit -m "Projekte: Services als untereinander stehende Abschnitte mit Zeilen, Preview-Zustand als Badge" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 14: Gate P5, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0 und `# fail 0`. `# pass` ist die Zahl aus Task 0 plus 13 (liegen neue Commits auf `test`, kann sie höher sein). Die Bundle-Größen aus der Ausgabe werden notiert.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p5 && "$W/shots.sh" p5 "$W/shots/p5" --gate p5`
|
||||
Expected:
|
||||
- Exit 0. Das Gate `p5` gilt für alle Ansichten (`views: '*'`), also auch für die aus P0 bis P4. So zeigt der Lauf, dass P5 sonst nichts verschlechtert hat.
|
||||
- Keine Verstöße gegen `external-request`, `console-error`, `step-failed`, `dark-surface`, `low-contrast` und `overflow-x`.
|
||||
- `icon-svg` ist für alle Ansichten im Umfang des Gates (P2 bis P5, ohne `ignore`) sauber.
|
||||
|
||||
- [ ] **Step 3: Code-Hygiene und Klassenabgleich**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|btn-sm|style=\{|#[0-9a-fA-F]{3,6}\b|text-grey|text-red|bg-red|ui-btn|ui-card|form-control|badge-(ok|info|warn|muted|danger)|isInteractiveTarget" src/pages/ProjectsPage.jsx src/pages/AssignProjectPage.jsx src/pages/ServicesPage.jsx src/components/TicketProjects.jsx src/components/AssignedProjectCard.jsx src/components/AnalyseRow.jsx src/components/PreviewBlock.jsx src/components/PreviewLinkButton.jsx src/components/GiteaLinkButton.jsx src/components/CopyableCredential.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "btn-sm" src`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "dotColor" src`
|
||||
Expected: nur noch `src/lib/previewEnv.js` (P10 löscht es samt Test).
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" > "$TMPDIR/klassen.json" && node -e "const j=JSON.parse(require('fs').readFileSync(process.argv[1]));const mine=/^(projects-table|row-actions|pv-|proj-|dialog-form|pick-|summary|ticket-projects-assign)/;for (const k of ['undefinedUsed','dead']) console.log(k, j[k].filter((c)=>mine.test(c)).join(' '))" "$TMPDIR/klassen.json"`
|
||||
Expected: Hinter `undefinedUsed` und hinter `dead` steht kein Name. Keine Klasse des P5-Blocks ist unbenutzt oder ohne Regel (`projects-table`, `row-actions`, `pv-*`, `proj-*`, `dialog-form`, `pick-*`, `summary`, `ticket-projects-assign`).
|
||||
|
||||
- [ ] **Step 4: Bundle-Größe**
|
||||
|
||||
Die Werte aus der `check.sh`-Ausgabe von Step 1 werden mit `design-p4` verglichen. JS ist kleiner, weil `ProjectsPage`, `AssignedProjectCard`, `GiteaLinkButton` und weitere keine Icon-Importe mehr haben.
|
||||
|
||||
- [ ] **Step 5: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p4..HEAD` gegen die Spec-Abschnitte „Bausteine“, „Ticket als eigene Seite / Route und Links“, „Texte“, „Was gleich bleibt“ und Paket P5, dazu der Review Focus oben. Besonders:
|
||||
- Jede Steuerung aus den Tabellen der Tasks 7 bis 13 ist erreichbar.
|
||||
- Es gibt keinen Textunterschied außer den Spec-Texten und den „Entscheidung“-Zeilen (Stichprobe: `git diff design-p4..HEAD -- src/pages src/components` auf geänderte Texte lesen).
|
||||
- Alle `window.confirm`, `busyRef`-artigen Sperren (`assignBusyId`, `hostBusyId`, `refreshBusyId`, `archiveBusy`) und `data-testid`/`data-hidden-hits` sind erhalten.
|
||||
|
||||
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only projekte,services,ticket-projekte`.
|
||||
|
||||
- [ ] **Step 6: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p5 && git log --oneline origin/test..design-p5 && git rev-parse design-p5`
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- die Screenshots aus `$W/shots/p5/1440x900` und `$W/shots/p5/390x844` (Auswahl: `projekte`, `projekte-lang`, `projekte-861`, `dlg-projekt-neu`, `dlg-projekt-bearbeiten`, `zuordnen-kunde`, `zuordnen-fertig`, `services`, `analyse-zustimmung`, `ticket-projekte`)
|
||||
- die Liste der „Entscheidung“-Zeilen dieses Plans, vor allem: neues Wort „Ausgewählt“, Preview-Badge `Preview: {Zustand}` in den Services, gestapelte Zeilen der Projekttabelle unter 860 px, Dialogtitel „Website-Analyse“
|
||||
- den Hinweis, dass „Zuordnen“ in den Services jetzt ein Textlink ist
|
||||
- für P8: Von `.credential*` bleiben `.credential`, `.credential-value`, `.credential-value.is-masked`, `.credential-empty`, `.credential .link`
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p5^{commit}:refs/heads/test'`
|
||||
- die Rücknahme `git revert --no-commit design-p4..design-p5`, Commit auf `test` und Push durch den Eigentümer (nur solange P5 das jüngste Paket ist)
|
||||
|
||||
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers: `/projects` (Reiter, eine Zeile mit „Aktualisieren“ oder „Hosten“) und die Projekte eines Tickets. Nichts anklicken, was speichert.
|
||||
@@ -0,0 +1,611 @@
|
||||
# WOMS „Weißes Design“ P6: Kunden-Unterseiten – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Chat, Gutschriften, Abo-Angebote und E-Mail-Speicher sind hell und kastenlos. Die Chat-Liste besteht aus Haarlinien-Zeilen, die drei Listen sind Tabellen mit Spaltenpriorität, das Angebots-Formular ist ein Dialog. Der Chat wechselt bei 860 px zwischen Liste und Verlauf. `admin-areas.css` ist am Ende gelöscht, weil P4 die Kundenseite davon gelöst hat und P6 die letzten Nutzer umbaut.
|
||||
|
||||
**Architecture:**
|
||||
- `chat.css` wird neu geschrieben. Jede Regel für Elemente im Chat-Container trägt das Präfix `.woms-chat` und ist strikt spezifischer als die Fremdregel des Portal-Bausteins. Das Seitenlayout (`.chat-page` usw.) bleibt ohne Präfix.
|
||||
- Die Listen nutzen `Tabs`, `.table` mit `.hide-md`/`.hide-sm`, `Badge`, `EmptyState`. `CreditDetail` wird `.two-col` mit `Card`-Abschnitten und `.kv`. `OfferForm` wird ein `Dialog`.
|
||||
- Neue reine Helfer liegen in `src/lib` und sind per Test gesichert. Das Screenshot-Werkzeug bekommt den echten Portal-Chat-Baustein, damit die Chat-Regeln gegen das Fremd-CSS geprüft werden.
|
||||
|
||||
**Tech Stack:** React 18, react-router-dom 6, reines CSS, `node --test`, Prüf- und Screenshot-Werkzeug aus P0 (`env.sh`, `check.sh`, `shots.sh`).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“, „Übergangsschicht KOMPAT“, Paket P6, „Seiten ohne Prototyp-Vorlage“, „Texte“, „Was gleich bleibt“, Risiken (Chat-Composer, Vorschlagslisten). Anhang `…-umbaukarte.md`: Abschnitt 5 (P6), 7.2, 8.1 Punkte 13, 14, 19. Globale Regeln: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Es gelten die Regeln der Übersicht, besonders „Befehle und Prüfungen“, „CSS-Reihenfolge“, „Parallele Arbeit an WOMS“, „Paketstart und Abnahme-Reihenfolge“ und „Bausteine benutzen“. Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Für P6 kommt hinzu:
|
||||
|
||||
- **Voraussetzung:** Tag `design-p5` ist gesetzt. P4 hat `CustomerFunktionen`, `AboStatusCard` und `KundenansichtButton` umgebaut, alle `.funktionen-*`-Regeln nach `global.css` verlegt und die beiden Dateien von `admin-areas.css` gelöst (keine `.area-*`-Klasse, kein Import). Task 1 prüft das.
|
||||
- **Dateien:** nur `CustomerChatPage`, `CreditsPage`, `OffersPage`, `StorageTasksPage`, `src/components/{chat,credits,offers}/*`, `src/styles/{chat,admin-areas,global}.css`, `src/lib/{credit,offer,storage}Format.js` samt Tests und `$W`. Nie anfassen: `loadChatComposer.js` samt Test, die Container in `ChatThread.jsx`, das Portal-Repo (nur Lesezugriff für den Snapshot in Task 2), die P4-Dateien.
|
||||
- **Ladereihenfolge:** `main.jsx` importiert `App` vor `global.css`, also steht `chat.css` im Bundle vor `global.css`, und das Portal-CSS lädt zur Laufzeit zuletzt. Gleiche Spezifität verliert immer: `.btn` aus `global.css` überstimmt ein `.chat-back { display: none }`. Seitenregeln für Elemente mit globaler Klasse brauchen höhere Spezifität (`.chat-page .chat-back`), nie `!important`.
|
||||
- **Texte:** Erlaubt sind nur „← Alle Unterhaltungen“ (Pfeil statt Icon), „Speichert …“ und „Wird geladen …“ (statt Spinner). Alles andere bleibt wörtlich, auch Spaltenköpfe, Hinweisabsätze und alle `confirm`/`alert`-Texte.
|
||||
- **Hygiene und Inventur:** In den zehn P6-Dateien gibt es danach kein `react-icons`, kein `size="sm"`, kein `style={{` (heute 26). Treffer je Datei für `window.confirm`, `window.alert`, `busyRef`, `data-storage-done`, `id="calc-*"`, `id="credit-*"` bleiben: `CreditDetail.jsx` 16 (1 confirm, 9 busyRef, 6 IDs), `OfferList.jsx` 4, `OfferForm.jsx` 1, `StorageTasksPage.jsx` 3.
|
||||
|
||||
**Entscheidungen** (die Spec ist dazu still):
|
||||
- **Entscheidung:** Die Prüf-Zeichen `✓`, `!`, `i` der „Automatischen Prüfung“ bleiben als Badge-Text. Die Umbaukarte sagt „ohne Glyphen“, aber die Spec „Texte“ erlaubt keine Textänderung dafür, und sonst trüge nur die Farbe den Status.
|
||||
- **Entscheidung:** Die Filter sind `Tabs` ohne Zähler (die Spec nennt keine, es gibt keine neuen Abfragen).
|
||||
- **Entscheidung:** Tabellenzeilen sind nicht klickbar (heute auch nicht), kein `tr.row` und damit kein `isRowActivation` (`src/lib/rowClick.js` aus P2). Aktionen stehen als `button.link` bzw. `.link-danger` in der letzten Zelle. Die offene Gutschrift trägt `aria-current="true"`.
|
||||
- **Entscheidung:** Spaltenpriorität (immer / `hide-sm` / `hide-md`):
|
||||
- Gutschriften: Kunde, Status, Aktion / Eingang, Rechnung / Leistung, Bezahlt bis.
|
||||
- Angebote: Kunde, Angebot, Status mit Aktionen / Gültig bis / Angelegt.
|
||||
- Speicher: Kunde, Aufgabe, Aktion / Fällig / Bestellt.
|
||||
- **Entscheidung:** Der Chat-Zähler ist eine kleine Zahl in `--accent`, fett (wie `.nav-count.hot`), keine Pille. Die aktive Zeile hat `aria-current="true"` und `--accent-soft`.
|
||||
- **Entscheidung:** Beim Portal-Composer bleibt die Geometrie (46 px, runde Knöpfe). Überschrieben werden Farben, Ring statt Rand, Hover, Fokus, Deaktiviert und unter 640 px wieder `font-size: 16px` im Eingabefeld (das heutige `font: inherit` hebt die Portal-Regel gegen das iOS-Zoomen auf). Der Datei-Chip ist weiß mit Ring, weil die Blase selbst `--bg-hover` ist. Die Anhang-Menü-Icons bleiben.
|
||||
- **Entscheidung:** Lightbox und Kamera bleiben unberührt dunkel (am `body`, vom Präfix nicht erreichbar). Der Kamera-Knopf „Foto verwenden“ bleibt im Portal-Lila und geht als „bewusst offen“ in die Übergabe.
|
||||
- **Entscheidung:** `OfferForm` bleibt ohne `<form>` (also kein `Dialog as="form"`, kein Enter-Submit, wie heute). Beide Fußknöpfe tragen trotzdem `type="button"`. „Abbrechen“ ist beim Speichern gesperrt, passend zu „ד und Esc.
|
||||
- **Entscheidung:** `admin-areas.css` wird in P6 gelöscht. P4 hat die `.funktionen-*`-Regeln nach `global.css` verlegt und `CustomerFunktionen` und `AboStatusCard` von der Datei gelöst. Die P6-Seiten verlieren ihre `.area-*`-, `.credit-*`-, `.offer-*`- und `.storage-row`-Klassen und den Import. Danach hat die Datei keinen Nutzer mehr (Task 11).
|
||||
- **Entscheidung:** Kleine Ergänzungen stehen in einem Block „Kunden-Unterseiten (P6)“ am Ende von `global.css`. `table-wrap` gilt nur für die drei P6-Tabellen, damit P2 bis P5 unberührt bleiben.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Aus der Übersicht gehören zu P6:
|
||||
- **Punkt 2, lange Inhalte am Handy:** Kundenname mit 60 Zeichen in allen Listen, E-Mail mit 50 Zeichen ohne Leerzeichen, URL mit 80 Zeichen im Chat, Angebotstext und interne Notiz je 200 Zeichen. `overflow-x` bei 390 px in allen P6-Ansichten.
|
||||
- **Punkt 3, Teilausfall:** Das Szenario `fehler-portal` lässt alle `/api/team/*`-Anfragen mit 500 antworten. Die vier Seiten zeigen `.msg.err` und bleiben bedienbar, „Erneut versuchen“ lädt neu.
|
||||
- **Punkt 4, nur Tastatur:** Tabellenaktion per Tab erreichbar. Esc schließt erst die Vorschlagsliste, dann den Dialog. Tab bleibt im Dialog, „ד und Esc sind beim Speichern gesperrt. Doppelklick auf „Genehmigen & gutschreiben“ löst einen POST aus, der 300-ms-Debounce einen Berechnungs-POST.
|
||||
- **Punkt 5, viele Einträge:** `chat-viel` (150 Unterhaltungen, die Liste scrollt in sich) und `gutschriften-viel` (120 Anträge).
|
||||
|
||||
Dazu gilt für P6:
|
||||
- **Fremd-CSS gewinnt nicht:** `chat-verlauf` liest berechnete Stile und vergleicht sie mit Sollwerten (Task 4). `.chat-back` ist bei 1440 px unsichtbar, bei 390 px sichtbar.
|
||||
- **Vorschlagsliste im Dialog** ist vollständig erreichbar, der längste Speichern-Text passt bei 390 px in die Fußzeile.
|
||||
- **Zwischenstufe 1000 px:** Die Ansichten `*-tablet` zeigen die Spaltenpriorität.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Ausgangslage und Lib-Helfer
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/creditFormat.js`, `src/lib/offerFormat.js`, `src/lib/storageFormat.js` (neue Exporte, Stand vor P6)
|
||||
- Test (neu): `tests/unit/creditFormat.test.mjs`, `tests/unit/offerFormat.test.mjs`, `tests/unit/storageFormat.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `fmtDate`, `fmtDateTime`, `formatCents` aus `src/lib/format.js`, `isOverdue`.
|
||||
- Produces:
|
||||
- `creditActionLabel(status) → 'Prüfen' | 'Ansehen'`
|
||||
- `creditStatusNote(r) → string`
|
||||
- `checkGlyph(status) → '✓' | '!' | 'i'`
|
||||
- `checkTone(status) → 'ok' | 'warn' | 'info' | 'muted'`
|
||||
- `filterOffers(offers, filter) → offers[]`
|
||||
- `offerRowActions(o) → { sendLabel: '' | 'E-Mail senden' | 'Erneut senden', canWithdraw: boolean }`
|
||||
- `offerMailLine(o) → null | { tone: 'muted' | 'warn', text }`
|
||||
- `storageDue(t, now) → { tone, label, sub, subMuted }`
|
||||
|
||||
- [ ] **Step 1: Paketstart und Ausgangslage prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git status --short && git fetch origin && git merge-base --is-ancestor design-p5 HEAD && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0, `git status --short` zeigt keine geänderten Dateien, `# fail 0`. Die Testzahl wird als N5 notiert. Schlägt `merge-base` fehl, fehlt `design-p5` im Branch: stoppen und melden.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/CustomerChatPage.jsx src/pages/CreditsPage.jsx src/pages/OffersPage.jsx src/pages/StorageTasksPage.jsx src/components/chat src/components/credits src/components/offers src/lib/creditFormat.js src/lib/offerFormat.js src/lib/storageFormat.js src/styles/chat.css src/styles/admin-areas.css src/styles/global.css`
|
||||
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt, TP2 zuerst nach `test` zu bringen.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|ui-btn|FaSpinner" src/components/customer/KundenansichtButton.jsx src/components/customer/AboStatusCard.jsx src/components/customer/CustomerFunktionen.jsx; rg -n "admin-areas|\barea-" src/components/customer; rg -n "funktionen-" src/styles/admin-areas.css`
|
||||
Expected: keine Treffer. Ein Treffer heißt, dass P4 `CustomerFunktionen` oder `AboStatusCard` nicht vollständig von `admin-areas.css` gelöst hat: stoppen und melden, die P4-Dateien werden in P6 nicht umgebaut.
|
||||
|
||||
- [ ] **Step 2: Failing Tests schreiben** (Leerzeichen in Beträgen ist U+00A0)
|
||||
|
||||
`creditFormat.test.mjs` (4 Tests):
|
||||
- `creditStatus` pinnt die Tonnamen: `offen` → `{ tone: 'warn', label: 'Zu prüfen' }`, `rueckfrage` → `muted`/„Rückfrage beim Kunden“, `genehmigt` → `ok`/„Genehmigt“, `abgelehnt` → `danger`/„Abgelehnt“, `'neu'` → `muted`/`'neu'`.
|
||||
- `creditActionLabel`: `offen`, `rueckfrage` → `'Prüfen'`, sonst `'Ansehen'`.
|
||||
- `creditStatusNote`: `{ status: 'genehmigt', amountCents: 1250, appliedAt: '2026-10-01T10:00:00Z' }` → `'12,50 €'`, ohne `appliedAt` → `'12,50 € · nicht gebucht'`, `{ status: 'offen', amountCents: 1250 }` → `''`.
|
||||
- `checkGlyph`/`checkTone`: `ok` → `'✓'`/`'ok'`, `warn` → `'!'`/`'warn'`, `info` → `'i'`/`'info'`, `undefined` → `'i'`/`'muted'`.
|
||||
|
||||
`offerFormat.test.mjs` (4 Tests):
|
||||
- `offerStatus`: `offen` → `warn`/„offen“, `aktiv` → `ok`/„aktiv“, `abgelaufen` → `muted`/„abgelaufen“, `zurueckgezogen` → `muted`/„zurückgezogen“.
|
||||
- `filterOffers` mit Status `offen, aktiv, abgelaufen, zurueckgezogen` (ids `1` bis `4`): `'open'` → `['1','2']`, `'all'` → alle, `null` → `[]`, Eingabe unverändert.
|
||||
- `offerRowActions`: `{ status: 'offen', mailSendable: true }` → `{ sendLabel: 'E-Mail senden', canWithdraw: true }`, mit `notifiedAt` → `'Erneut senden'`, `{ status: 'aktiv', mailSendable: true }` → `canWithdraw: false`, `{ status: 'abgelaufen', mailSendable: false }` → `{ sendLabel: '', canWithdraw: true }`, `{ status: 'zurueckgezogen' }` → `{ sendLabel: '', canWithdraw: false }`.
|
||||
- `offerMailLine`: `{ notifiedAt: '2026-09-30T10:05:00.000Z' }` → `{ tone: 'muted', text: 'E-Mail gesendet 30.09.26, 12:05' }`, `{ mailSendable: true }` → `{ tone: 'warn', text: 'E-Mail nicht verschickt' }`, `{ status: 'offen', customerEmail: '' }` → `{ tone: 'warn', text: 'Keine E-Mail-Adresse hinterlegt' }`, `{ status: 'abgelaufen', customerEmail: '' }` und `{ status: 'offen', customerEmail: 'a@b.de' }` → `null`.
|
||||
|
||||
`storageFormat.test.mjs` (2 Tests, `t0 = Date.parse('2026-10-05T08:00:00Z')`):
|
||||
- `storageDue`: `{ kind: 'einrichten', createdAt: '2026-10-01T08:30:00.000Z' }` → `{ tone: 'warn', label: 'jetzt einrichten', sub: 'bestellt 01.10.26, 10:30', subMuted: true }`. `{ kind: 'senken', dueAt: '2026-10-04T00:00:00.000Z' }` → `{ tone: 'warn', label: 'zum Periodenende', sub: '04.10.2026', subMuted: false }`, mit `dueAt: '2026-10-20T00:00:00.000Z'` → `tone: 'muted'`, `sub: '20.10.2026'`.
|
||||
- `isOverdue`: vergangenes `dueAt` → `true`, zukünftiges oder fehlendes → `false`.
|
||||
|
||||
- [ ] **Step 3: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/creditFormat.test.mjs tests/unit/offerFormat.test.mjs tests/unit/storageFormat.test.mjs`
|
||||
Expected: FAIL (`… is not a function`).
|
||||
|
||||
- [ ] **Step 4: Implementieren**
|
||||
|
||||
Die Regeln sind die heutigen Inline-Ausdrücke, herausgezogen: `creditActionLabel` aus `CreditList.jsx` Z. 52, `creditStatusNote` Z. 48, `checkGlyph` und `checkTone` aus `CreditDetail.jsx` Z. 11 und 211, `filterOffers` aus `OfferList.jsx` Z. 54, `offerRowActions` und `offerMailLine` Z. 93 bis 109 (Reihenfolge der Mail-Zeile: `notifiedAt`, `mailSendable`, `offen` ohne Adresse, sonst `null`), `storageDue` aus `StorageTasksPage.jsx` Z. 57 bis 59. Zeichenketten bleiben wörtlich.
|
||||
|
||||
- [ ] **Step 5: Tests grün und Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-tests`
|
||||
Expected: Exit 0, `# pass` = N5 + 10, `# fail 0`.
|
||||
|
||||
```bash
|
||||
git add src/lib/creditFormat.js src/lib/offerFormat.js src/lib/storageFormat.js tests/unit/creditFormat.test.mjs tests/unit/offerFormat.test.mjs tests/unit/storageFormat.test.mjs
|
||||
git commit -m "Design P6: reine Helfer fuer Gutschrift-, Angebots- und Speicherzeilen mit Tests" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 2: Werkzeug: echter Chat-Baustein, Fixtures, Ansichten, Gate p6
|
||||
|
||||
**Files** (unter `$W/`, außerhalb des Repos, kein Commit): Create `fixtures/portal/{chat-composer.css,chat-composer.js,README.md}`. Modify `backend.mjs`, `fixtures/standard.mjs`, `checks.mjs`, `views.mjs`, `gates.mjs`.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createBackend`, `VIEWS`, `GATES`, `runChecks` aus P0, Gate `p5`. `ctx.backend.calls` (Liste `{ method, url, body }` aller beantworteten Anfragen).
|
||||
- Produces: Szenario `fehler-portal`, die Ansichten unten, Gate `p6`.
|
||||
|
||||
- [ ] **Step 1: Portal-Baustein als Snapshot**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cp /home/webklar/apps/kundenbereich.webklar.com/build/public/chat-composer.{css,js} "$W/fixtures/portal/" && sha256sum "$W"/fixtures/portal/chat-composer.*`
|
||||
Expected: css `b34fbaeb72922ddfbd57f1074e58e58dffc560cd1e9a31c98b12e66487885647`, js `ebb79581f9b307b303877f169fa5546bb98b4fc66320c40ae8fd78c965bace23`. Die README nennt Quelle, Datum 2026-10-05 und beide Hashes.
|
||||
|
||||
- [ ] **Step 2: Backend erweitern**
|
||||
|
||||
- `https://portal.test/chat-composer.css` und `.js` liefern den Snapshot (statt der leeren Dateien aus P0).
|
||||
- Anhang-Adressen unter `https://portal.test/api/team/files/*` liefern ein 1×1-PNG bzw. 200 ohne Inhalt.
|
||||
- `POST …/chats/:id/attachments` antwortet im Szenario `fehler-portal` und in `chat-anhang-fehler` mit 500.
|
||||
- `POST …/offers` und `POST …/takeover-credits/:id/decision` antworten nach 800 ms.
|
||||
- Szenario `fehler-portal`: jede Anfrage an `https://portal.test/api/team/*` bekommt 500 im Fehlerformat des Szenarios `fehler`. Deren Konsolenmeldungen gelten als absichtlich.
|
||||
|
||||
- [ ] **Step 3: Fixtures ergänzen** (Schlüssel nach dem Muster der vorhandenen, Formen laut Lesecode in den `*Format.js` und JSX-Dateien)
|
||||
|
||||
| Bereich | Fälle |
|
||||
|---|---|
|
||||
| Chats | 8 Unterhaltungen, 3 mit `unreadFromCustomer` (1, 12, 120), ein 60-Zeichen-Name, ein `lastText` mit 80-Zeichen-URL. Satz `viel` mit 150. |
|
||||
| Verlauf Kunde A | 7 Nachrichten: Kundentext, Teamtext (`senderName` „Kenso Grimm“), Bild (`kind: 'image'`, 640×480), PDF (`Angebot-Kuehn-2026.pdf`, 183000 Byte), Sprachnachricht (`kind: 'voice'`, `durationMs: 23000`), Teamtext mit 80-Zeichen-URL, langer Kundentext. |
|
||||
| Gutschriften | 5 Anträge (`offen`, `rueckfrage`, `genehmigt` gebucht, `genehmigt` ungebucht, `abgelehnt`), einer mit 60-Zeichen-Name und langem `services`/`provider`/`domain`. Satz `viel` mit 120. `getCredit` für einen offenen und einen ungebuchten genehmigten Antrag: 3 `requiredChecks`, `autoChecks` mit `ok`/`warn`/`info` (mit `detail`), `calc`, laufendes Abo, 1 weiterer Antrag, 2 Dateien, 3 Verlaufseinträge. `POST …/calc` → `{ calc }`. |
|
||||
| Angebote | 6 Angebote: `offen` mit `mailSendable` ohne `notifiedAt`, `offen` ohne E-Mail, `aktiv` mit `scope: 'laufend'`, `abgelaufen` mit `mailSendable` und `notifiedAt`, `zurueckgezogen`, eins mit 60-Zeichen-Name, 50-Zeichen-E-Mail und 200-Zeichen-`internalNote`. `getOfferContext` für einen Kunden ohne Abo und einen mit laufendem Wartungs-Abo (`upcomingCharges`). |
|
||||
| Speicher | `tasks`: `einrichten`, `senken` überfällig, `senken` später, einer `partial` mit `error`, einer mit 60-Zeichen-Name. `failed` mit 2 Einträgen, `allocatedGb` über `warnGb`. |
|
||||
|
||||
- [ ] **Step 4: Prüf-Ausnahmen erweitern**
|
||||
|
||||
- In `checks.mjs`: `dark-surface` nimmt zusätzlich `.woms-chat :is(.chat-send, .chat-audio-play, .chat-file-ext, .chat-tray-remove, .chat-tray-retry, .chat-tray-progress)` aus. Der Szenario-Name `fehler-portal` gilt für `console-error` wie `fehler` (erwartete 500-Meldungen ausgenommen).
|
||||
- `icon-svg` ist dauerhaft für alles unter `.woms-chat` ausgenommen (Sprechblasen mit Anhängen zeichnet derselbe fremde Baustein). Das steht im Gate `p6` (Step 5) als `ignore`, nicht in `checks.mjs`.
|
||||
|
||||
- [ ] **Step 5: Ansichten und Gate eintragen**
|
||||
|
||||
`views.mjs` bekommt die Hilfe `tablet(page)`: ab 800 px Breite `setViewportSize({ width: 1000, height: 800 })`, sonst nichts. Die Schritte haben die Signatur `steps(page, ctx)`, Anfragen liest man aus `ctx.backend.calls`. Prüfungen in Schritten nutzen `assert` aus `node:assert/strict`. Eine Ausnahme erzeugt den Verstoß `step-failed` mit Ansicht und Meldung (das Gate endet mit Exit 1). Die Kundenseiten-Ansichten tragen die IDs der Übersicht (`kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`), es gibt keine zweite Ansicht für dieselbe Reiter-URL.
|
||||
|
||||
| ID | Pfad / Schritte | Prüfung im Schritt |
|
||||
|---|---|---|
|
||||
| `chat-verlauf` | `/customers/chat?kunde=<A>` | `.chat-back` bei 1440 `display: none`, bei 390 sichtbar. `.chat-side` bei 390 ausgeblendet. Stil-Sollwerte aus Task 4. |
|
||||
| `chat-menue`, `chat-anhang-fehler` | wie `chat-verlauf`; Klick `.chat-attach-btn` bzw. `setInputFiles` (`x.pdf`) auf `.chat-composer input[type=file]:not([accept])` | `.chat-menu` sichtbar bzw. `.chat-tray-item.is-error` und `.chat-composer-note` sichtbar |
|
||||
| `chat-tablet`, `gutschriften-tablet`, `angebote-tablet` | Ansicht mit `tablet` | Chat: Liste und Verlauf nebeneinander, `.chat-input-row` ohne Überlauf. Tabellen: `.hide-md`-Spalten aus, `.hide-sm`-Spalten sichtbar |
|
||||
| `chat-viel`, `gutschriften-viel` | Satz `viel` | `.chat-list.scrollHeight > clientHeight` |
|
||||
| `chat-fehler`, `gutschriften-fehler`, `angebote-fehler`, `speicher-fehler` | Szenario `fehler-portal` | `.msg.err` sichtbar; bei Chat und Speicher Klick auf „Erneut versuchen“, die Seite lebt |
|
||||
| `gutschriften-antrag` | `/customers/gutschriften?antrag=<offen>` | – |
|
||||
| `gutschriften-sperre` | wie oben; `page.on('dialog', d => d.accept())`; 3 Kästchen, `#credit-note` füllen, Doppelklick „Genehmigen & gutschreiben“; in `#calc-amount` 3 Ziffern mit 50 ms Abstand | nach 1500 ms genau 1 POST `…/decision` und 1 POST `…/calc` in `ctx.backend.calls` (`method`, `url`) |
|
||||
| `angebote-dialog`, `angebote-vorschlag`, `angebote-laufend` | Klick „Neues Angebot“; im Kunden-Feld „Mu“ tippen; Kunde mit laufendem Abo wählen | `[role=dialog]` sichtbar; letzter `.autocomplete-item` per `elementFromPoint` erreichbar; bei 390 px `.dialog-foot.scrollWidth <= clientWidth` |
|
||||
| `angebote-tastatur` | `page.on('dialog', d => d.accept())`. Bis zu 15 Mal Tab, bis der Fokus in `table` liegt. Dialog öffnen, Kunden-Feld tippen, Esc, Esc. Erneut öffnen, 30 Mal Tab. Kunde wählen, „Angebot anlegen“, während der 800 ms Esc | Fokus erreicht die Tabelle. Erstes Esc schließt nur die Liste, zweites den Dialog. Fokus bleibt im Dialog. Beim Speichern ist `.dialog-close` `disabled` und der Dialog nach Esc noch da |
|
||||
|
||||
Dazu die vorhandenen Ansichten `chat`, `gutschriften`, `angebote`, `speicher`, `kunde-chat`, `kunde-abo`, `kunde-gutschriften`, `kunde-funktionen`. In `gates.mjs` entsteht `p6` nach dem Schema der Übersicht: das Gate `p5` plus `icon-svg` für die Ansichten von P6 und aller früheren Pakete.
|
||||
|
||||
```js
|
||||
const P6_VIEWS = ['chat', 'chat-verlauf', 'chat-menue', 'chat-anhang-fehler', 'chat-tablet', 'chat-viel', 'chat-fehler',
|
||||
'gutschriften', 'gutschriften-tablet', 'gutschriften-viel', 'gutschriften-fehler', 'gutschriften-antrag', 'gutschriften-sperre',
|
||||
'angebote', 'angebote-tablet', 'angebote-fehler', 'angebote-dialog', 'angebote-vorschlag', 'angebote-laufend', 'angebote-tastatur',
|
||||
'speicher', 'speicher-fehler', 'kunde-chat', 'kunde-abo', 'kunde-gutschriften', 'kunde-funktionen']
|
||||
const prev = GATES.p5.scoped['icon-svg']
|
||||
GATES.p6 = {
|
||||
...GATES.p5,
|
||||
scoped: { ...GATES.p5.scoped, 'icon-svg': { views: [...new Set([...prev.views, ...P6_VIEWS])], ignore: [...(prev.ignore ?? []), '.woms-chat'] } },
|
||||
}
|
||||
```
|
||||
|
||||
`step-failed`, `external-request` und `console-error` sind wie in jedem Gate aktiv, `dark-surface`, `low-contrast` und `overflow-x` kommen über `GATES.p5.checks` für alle Ansichten.
|
||||
|
||||
- [ ] **Step 6: Rot-Beweis**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t2" --only chat-verlauf,angebote --gate p6; echo "exit $?"`
|
||||
Expected: `exit 1` mit `icon-svg` mindestens in `angebote` (`FaPlus`). Das Bild `chat-verlauf` zeigt den echten Composer mit Anhängen.
|
||||
|
||||
### Task 3: Globale Ergänzungen
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css` (neuer Block am Ende)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `.table.table-wrap td` (lange Wörter brechen), `.table tr[aria-current="true"] td`, `.dialog-foot { flex-wrap: wrap }`, `.autocomplete .input { width: 100% }` (P7 baut auf die beiden letzten).
|
||||
|
||||
- [ ] **Step 1: Block anlegen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "aria-current|dialog-foot|\.autocomplete \.input" src/styles/global.css`
|
||||
Expected: zeigt, welche Regeln P1 bis P5 schon angelegt haben. Der Block steht am Dateiende, hinter allen anderen Regeln, damit er bei gleicher Spezifität gewinnt. Unter `/* ===== Kunden-Unterseiten (P6) ===== */` stehen nur die fehlenden:
|
||||
- `.table.table-wrap td { overflow-wrap: anywhere }`
|
||||
- `.table tr[aria-current="true"] td { background: var(--bg-subtle) }`
|
||||
- `.dialog-foot { flex-wrap: wrap }`
|
||||
- `.autocomplete .input { width: 100% }`
|
||||
|
||||
- [ ] **Step 2: Bauen und Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
|
||||
Expected: Exit 0, die Bundle-Größen werden ausgegeben.
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css
|
||||
git commit -m "Design P6: kleine Tabellen-, Dialog- und Autocomplete-Ergaenzungen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 4: `chat.css` neu, Fehlerzeile im Verlauf
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/chat.css` (komplett neu), `src/components/chat/ChatThread.jsx` (Import Z. 2, Fehlerzeile Z. 120 bis 125)
|
||||
- Test: `tests/unit/chatCss.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tokens der Spec (`--bg`, `--bg-hover`, `--bg-subtle`, `--grey-bg`, `--text*`, `--line*`, `--accent*`, `--red-fg`, `--danger`, `--radius*`, `--ring`, `--shadow-pop`), `.msg.err`, `Button`.
|
||||
- Produces: `.woms-chat` mit `--chat-bubble-in`, `--chat-bubble-out`, `--chat-track`; Seitenklassen `.chat-page`, `.chat-side`, `.chat-list`, `.chat-list-item`, `.chat-list-top`, `.chat-list-last`, `.chat-list-time`, `.chat-unread`, `.chat-main`, `.chat-back`.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
`chatCss.test.mjs` liest `chat.css` als Quelltext (Vorbild `adminRoute.test.mjs`), entfernt Kommentare und zerlegt per `/([^{}]+)\{/g` in Selektoren (ohne `@…`, Kommas gesplittet). 5 Tests:
|
||||
1. Jeder Selektor mit `/\.chat-(bubble|meta|messages|notice|input-row|icon-btn|send|menu|tray|rec|audio|att|file|composer|drop-active)\b/` beginnt mit `.woms-chat` (`deepEqual(bad, [])`).
|
||||
2. Kein `!important`.
|
||||
3. `match(/@media \(max-width: 860px\)/)` und `doesNotMatch(/720px/)`.
|
||||
4. `ChatThread.jsx` enthält unverändert `className="woms-chat" ref={zoneRef}`, `className="chat-messages" ref={boxRef}` und `<div ref={composerRef} />`.
|
||||
5. `match(/\.chat-page \.chat-back\s*\{[^}]*display:\s*none/)`.
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/chatCss.test.mjs`
|
||||
Expected: FAIL bei 3 (alter 720-px-Wert) und 5. Test 4 besteht schon.
|
||||
|
||||
- [ ] **Step 3: `chat.css` neu schreiben**
|
||||
|
||||
Der Kopfkommentar nennt: Das Portal-CSS lädt zur Laufzeit zuletzt, Gleichstand verliert, deshalb ist jede Regel strikt spezifischer und es gibt kein `!important`. Alle P1-Variablen und -Fixes aus P1 Task 9 Step 2 bleiben unverändert. Neu sind nur `--chat-bubble-in: var(--bg-hover)`, `--chat-bubble-out: var(--grey-bg)` und `--chat-track: rgba(26,31,54,.2)`. Maße, Abstände, Schriftgrößen und Radien folgen dem Prototyp und den Tokens der Spec und entstehen beim Bauen. Verbindlich sind die Spezifitäten und Abweichungen der Tabellen sowie die Messwerte aus Step 6.
|
||||
|
||||
Überschreibungen des Portal-Bausteins. Die Spalte „Eigen > Fremd“ nennt die Spezifität der eigenen Regel vor der überstimmten Portal-Regel:
|
||||
|
||||
| Ziel | Selektor (Eigen > Fremd) | Abweichung vom Portal-Stil |
|
||||
|---|---|---|
|
||||
| Container, Verlauf | `.woms-chat`, `.woms-chat .chat-messages` | Flex-Spalte, `min-width: 0`. Verlauf scrollt in sich (`max-height: 60vh`), ohne Rand und Hintergrund. |
|
||||
| Blase, Zeit | `.woms-chat .chat-bubble` (0,2,0 > 0,1,0), `.chat-bubble.mine`, `.chat-meta`, `.chat-notice` | Fremde Blase `--chat-bubble-in`, eigene `--chat-bubble-out` (rechts), kein Rand. `max-width: 78%`, `overflow-wrap: anywhere`. Zeit und Hinweis 12 px in `--text-2`. |
|
||||
| Datei-Chip | `.woms-chat .chat-att-file` (0,2,0 > 0,1,0), `:hover` (0,3,0 > 0,2,0) | `var(--bg)`, `border: 0`, `box-shadow: var(--ring)`. Hover `--bg-hover`. |
|
||||
| Audio, Fokus | `.chat-audio-bar` (0,2,0 > 0,1,0); `.chat-att-image:focus-visible`, `.chat-audio-bar:focus-visible` (0,3,0 > 0,2,0); `.chat-bubble.mine .chat-audio-play` (0,4,0 > 0,3,0); `.chat-audio-dl:hover` (0,3,0 > 0,2,0); `.chat-audio.is-error .chat-audio-time` (0,4,0 > 0,3,0) | Leiste `--chat-track`. Fokusring `2px solid var(--accent)` statt weiß. Abspielen `--chat-accent` mit `--chat-on-accent`. Download-Hover `--accent-soft`. Fehlerzeit `--red-fg`. |
|
||||
| Eingabefeld | `.woms-chat .chat-input-row textarea` (0,2,1 > 0,1,1), `:focus` (0,3,1), Media `max-width: 640px` | `border: 0`, `box-shadow: var(--ring)`, `font: inherit`. Fokus `0 0 0 1px var(--accent), 0 0 0 4px var(--accent-ring)` ohne `outline`. Handy `font-size: 16px` (gegen das iOS-Zoomen). Höhe und Radius des Portals bleiben. |
|
||||
| Icon-Knöpfe, Senden | `.chat-icon-btn` (0,2,0 > 0,1,0), `:hover:not(:disabled)` (0,4,0 > 0,3,0), `[aria-expanded="true"]` (0,3,0 > 0,2,0), `.chat-icon-btn:disabled`, `.chat-send:disabled` (0,3,0 > 0,2,0), `.chat-send.is-busy:disabled` (0,4,0 > 0,3,0) | `border: 0`, `box-shadow: var(--ring)`, `var(--bg)`. Hover `--bg-hover`. Offen `--accent-soft` mit 1-px-Akzentring. Deaktiviert `opacity: .55; cursor: not-allowed`, beschäftigt `opacity: .8; cursor: progress`. Senden bleibt über die P1-Variablen schwarz mit weißer Schrift. |
|
||||
| Aufnahme | `.chat-rec .chat-icon-btn` (0,3,0 > 0,2,0), `:hover` (0,4,0 > 0,3,0); `.chat-rec-stop`, `.chat-rec-wave` (hinter der Icon-Knopf-Regel), `.chat-rec-dot` (0,2,0 > 0,1,0) | Knöpfe ohne Ring und Hintergrund, Hover `--accent-soft`. Farbe bzw. Punkt `--danger`. |
|
||||
| Anhang-Menü | `.chat-menu` (0,2,0 > 0,1,0); `.chat-menu button` (0,2,1 > 0,1,1), `:hover`, `:focus-visible` (0,3,1 > 0,2,1) | Menü ohne Rand, mit `--shadow-pop`. Hover und Fokus `--bg-hover`, Fokus zusätzlich `outline: 2px solid var(--accent); outline-offset: -2px` (die Fremdregel setzt `outline: none`). |
|
||||
| Vorschau, Hinweise | `.chat-tray-item` (0,2,0 > 0,1,0); `.chat-tray-item.is-error .chat-file-size` (0,4,0 > 0,3,0); `.chat-composer-note` (0,2,0 > 0,1,0), `.is-info` (0,3,0 > 0,2,0) | Vorschau `--bg-hover` mit transparentem Rand. Die rote Fehlervariante der Fremdregel (gleiche Spezifität, später geladen) bleibt absichtlich. Fehlertexte `--red-fg`, Info `--text-2`. |
|
||||
| Ablegen | `.woms-chat.chat-drop-active::after` (0,2,1 > 0,1,1) | helle Fläche `rgba(255,255,255,.92)`, Schrift `--text`, Rand `--text-3` |
|
||||
|
||||
Seitenlayout ohne Präfix. Es folgt dem Prototyp, verbindlich sind:
|
||||
- `.chat-page` ist ein Grid mit 320 px für die Liste und `minmax(0, 1fr)` für den Verlauf. Bei `max-width: 1100px` ist die Liste 260 px breit.
|
||||
- `.chat-side` und `.chat-main` haben `min-width: 0`. Zwischen ihnen steht eine senkrechte Haarlinie (`--line`).
|
||||
- `.chat-list` scrollt in sich (`max-height: 70vh; overflow-y: auto`). `.chat-list-item` ist eine Haarlinien-Zeile ohne Kasten und Rand, Hover `--bg-subtle`, `[aria-current="true"]` `--accent-soft`.
|
||||
- `.chat-list-last` kürzt mit Ellipsis. `.chat-unread` ist eine kleine Zahl in `--accent`, fett, 12 px, ohne Pille.
|
||||
- `.chat-page .chat-back { display: none }`, Spezifität (0,2,0) > `.btn` (0,1,0).
|
||||
- Bei `max-width: 860px` (nicht 720 px): einspaltig. `.chat-page.has-thread .chat-side` und `.chat-page:not(.has-thread) .chat-main` sind `display: none`, Seite und Haupt ohne Rand und Innenabstand, `.chat-page .chat-back { display: inline-flex }`, `.woms-chat .chat-bubble { max-width: 90% }`.
|
||||
|
||||
- [ ] **Step 4: Fehlerzeile im Verlauf**
|
||||
|
||||
In `ChatThread.jsx` bleibt der Zeilencontainer `flex items-center gap-2 wrap`. Der Text wird `<span className="msg err" role="alert">`, der Knopf `<Button onClick={…}>Erneut versuchen</Button>` ohne Icon. Der Import `react-icons/fa6` entfällt. Container, Effekte und `chat-notice muted` bleiben.
|
||||
|
||||
- [ ] **Step 5: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/chatCss.test.mjs tests/unit/loadChatComposer.test.mjs`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Gegen das echte Fremd-CSS prüfen**
|
||||
|
||||
`chat-verlauf` bekommt einen Schritt, der `getComputedStyle` liest und mit `assert.equal` vergleicht:
|
||||
- `.chat-bubble.theirs` `background-color` = `rgb(246, 248, 250)`, `.chat-bubble.mine` = `rgb(235, 238, 241)`.
|
||||
- `.chat-send` `background-color`/`color` = `rgb(17, 17, 17)`/`rgb(255, 255, 255)`.
|
||||
- `.chat-attach-btn` `border-top-width` = `0px`, `box-shadow` ≠ `none`.
|
||||
- `.chat-att-file` `background-color` = `rgb(255, 255, 255)`, `border-top-width` = `0px`.
|
||||
- `.chat-audio-bar` `background-color` = `rgba(26, 31, 54, 0.2)`.
|
||||
- `.chat-input-row textarea` `border-top-width` = `0px`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t4" --only chat-verlauf,chat-menue,chat-anhang-fehler --gate p6; echo "exit $?"`
|
||||
Expected:
|
||||
- Schritt-Prüfungen bestehen (kein `step-failed`). Der Bericht enthält höchstens `icon-svg` am alten Zurück-Pfeil von `CustomerChatPage` (Task 5). Nur dann ist `exit 1` hier erlaubt.
|
||||
- `chat-verlauf` bei 1440 px: Blasen hellgrau, Senden schwarz mit weißer Schrift, Icon-Knöpfe weiß mit Ring, Datei-Chip weiß.
|
||||
- `chat-anhang-fehler`: rote Fehlerzeile und rot umrandete Vorschau.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/chat.css src/components/chat/ChatThread.jsx tests/unit/chatCss.test.mjs
|
||||
git commit -m "Design P6: chat.css neu (Praefix .woms-chat, Umschaltung bei 860 px), Fehlerzeile ohne Icon" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 5: Chat-Seite
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/CustomerChatPage.jsx` (Z. 3, 61 bis 110), `src/styles/global.css` (KOMPAT-Regel `.side-count` löschen)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageHeader`, `Card`, `Button`, `EmptyState`, `AutocompleteInput` (Klasse `input`), `.msg.err`, Klassen aus Task 4, `lastLine`, `sortChats`.
|
||||
- Produces: Seite und Adresse `/customers/chat?kunde=<id>` unverändert.
|
||||
|
||||
- [ ] **Step 1: Seite umbauen**
|
||||
|
||||
- `react-icons`, `size="sm"` und `Card pad={false}` um die Liste entfallen. Die Liste ist `.chat-list` direkt unter dem Suchfeld.
|
||||
- Das Suchfeld bleibt `AutocompleteInput` („Neue Nachricht an Kunde…“, `onPick` öffnet nur die volle Beschriftung).
|
||||
- Fehlerzeile: `div.flex.items-center.gap-2.wrap` mit `span.msg.err[role=alert]` und `Button` „Erneut versuchen“. Der Leerzustand „Noch keine Unterhaltungen“ bleibt `EmptyState`.
|
||||
- Jede Unterhaltung ist ein `button.chat-list-item` mit `aria-current={c.customerId === kunde ? 'true' : undefined}`. Zeile 1 (`.chat-list-top`): `<strong>` Name und `.chat-list-time`. Zeile 2: `.chat-list-last` mit `lastLine(c)` und bei `unreadFromCustomer > 0` `span.chat-unread`.
|
||||
- Der Zurück-Knopf steht vor der Karte: `<Button className="chat-back" onClick={() => setParams({})}>← Alle Unterhaltungen</Button>`, darunter `<Card title={nameOf(kunde)}><ChatThread … /></Card>`. Ohne Auswahl bleibt die Karte mit `EmptyState` („Unterhaltung links auswählen“ / „Oder oben eine neue Nachricht an einen Kunden beginnen.“) und ohne Titel. Eine `Card` ohne Titel und ohne Aktionen rendert kein `h2` (Übersicht).
|
||||
|
||||
- [ ] **Step 2: `.side-count` löschen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "side-count" src`
|
||||
Expected: nur noch die KOMPAT-Regel in `global.css`. Sie wird gelöscht (P6 ist ihr letzter Nutzer), danach 0 Treffer.
|
||||
|
||||
- [ ] **Step 3: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t5" --only chat,chat-verlauf,chat-tablet,chat-viel,chat-fehler,kunde-chat --gate p6`
|
||||
Expected: Exit 0. Bei 390 px zeigt `chat` nur die Liste ohne Überlauf, `chat-verlauf` nur den Verlauf mit „← Alle Unterhaltungen“. Bei 1440 px zwei Spalten mit senkrechter Trennlinie, ohne Zurück-Knopf. `chat-tablet`: Liste 260 px.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/CustomerChatPage.jsx src/styles/global.css
|
||||
git commit -m "Design P6: Chat-Liste als Haarlinien-Zeilen, Zaehler und Zurueck-Pfeil als Text, side-count entfaellt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Gutschriften-Liste
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/CreditsPage.jsx` (Z. 10 bis 12), `src/components/credits/CreditsView.jsx` (Z. 25 bis 29), `src/components/credits/CreditList.jsx` (Z. 2, 7, 24 bis 62)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Tabs`, `Badge`, `EmptyState`; `.table`, `.table-wrap`, `.hide-sm`, `.hide-md`, `.right`, `.num`, `.cell-title`, `.cell-sub`, `.link`, `.loading`, `.msg.err`, `.table-foot`; Helfer aus Task 1.
|
||||
- Produces: `CreditList({ customerId, selectedId, onSelect, reloadKey })` mit unveränderten Props.
|
||||
|
||||
- [ ] **Step 1: `CreditList` umbauen**
|
||||
|
||||
- Filter `<Tabs tabs={[{ id: 'open', label: 'Offen' }, { id: 'all', label: 'Alle' }]} …/>`. State, Effekt und `listCredits` bleiben.
|
||||
- Laden `div.loading` „Wird geladen …“, Fehler `div.msg.err[role=alert]`, leer `EmptyState` mit „Keine offenen Anträge.“ bzw. „Noch keine Anträge.“.
|
||||
- `table.table.table-wrap` mit denselben Spalten und Zellinhalten wie heute. `<br />` plus `area-muted` wird `.cell-sub`, der Kundenname `.cell-title`. Klassen auf `th` und `td`: Eingang `hide-sm`, Leistung / bisher bei `hide-md`, Rechnung `hide-sm right num`, Bezahlt bis `hide-md`.
|
||||
- Statuszelle: `Badge` und, wenn nicht leer, `.cell-sub` mit `creditStatusNote(r)`. Letzte Zelle ohne Kopftext: `<button type="button" className="link" onClick={() => onSelect(r.id)}>{creditActionLabel(r.status)}</button>`.
|
||||
- Die Zeile der offenen Gutschrift trägt `aria-current="true"`. Imports `FaSpinner`, `Card`, `admin-areas.css` und die zwei Inline-Styles entfallen.
|
||||
|
||||
- [ ] **Step 2: `CreditsView` und `CreditsPage`**
|
||||
|
||||
`CreditsView`: Das `div` mit `marginBottom` um `CreditDetail` entfällt, die Liste steht in `<div className={selected ? 'mt-3' : undefined}>`. `CreditsPage`: Der Absatz „Übernahmegutschrift: …“ bleibt wörtlich als `<p className="table-foot">` ohne Inline-Style.
|
||||
|
||||
- [ ] **Step 3: Prüfen und Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t6" --only gutschriften,gutschriften-tablet,gutschriften-viel,gutschriften-fehler,kunde-gutschriften --gate p6`
|
||||
Expected: Exit 0. 390 px: Kunde, Status, Aktion, kein Überlauf mit dem 60-Zeichen-Namen. 1000 px: Eingang und Rechnung sichtbar, Leistung und Bezahlt bis aus. 1440 px: alle sieben Spalten.
|
||||
|
||||
```bash
|
||||
git add src/pages/CreditsPage.jsx src/components/credits/CreditsView.jsx src/components/credits/CreditList.jsx
|
||||
git commit -m "Design P6: Gutschriften-Liste als Tabelle mit Reitern und Spaltenprioritaet" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: `CreditDetail`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/credits/CreditDetail.jsx` (Imports Z. 1 bis 9, Markup Z. 116 bis 284)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Card`, `Badge`, `Button`, `Field`; `.two-col`, `.kv`, `.field-row`, `.check`, `.list-row`, `.cell-sub`, `.prose`, `.msg`, `.loading`, `.link`; `checkGlyph`, `checkTone`.
|
||||
- Produces: `CreditDetail({ id, onClose, onChanged, onOpen })` unverändert.
|
||||
|
||||
- [ ] **Step 1: Logik unverändert**
|
||||
|
||||
Nicht angefasst werden `load`, `figures`, `setFigure` (300-ms-Timer), `decide` (`decisionError`, `window.confirm`, `busyRef`), `apply`, `canApprove`, `amountTouched`. Neu ist nur der Zustand `busyAction` (`''`, `'approve'`, `'question'`, `'reject'`, `'apply'`), den `decide` und `apply` neben `busy` setzen und zurücksetzen, damit der gedrückte Knopf „Speichert …“ bzw. „Sendet …“ zeigen kann.
|
||||
|
||||
- [ ] **Step 2: Markup umbauen**
|
||||
|
||||
- **Kopf:** `<Card title={`Antrag ${r.ref} · ${c.name || c.companyName}`} actions={<Button onClick={onClose}>Schließen</Button>}>`, darunter `p.muted` „eingegangen … “ mit `Badge` und `p.msg.ok[role=status]` für `message`.
|
||||
- **Aufbau:** `div.two-col` mit zwei Spalten-`div`. Jede Gruppe ist ein `<Card title="…">` mit dem heutigen `h4`-Text. Den Abstand zwischen den Karten einer Spalte liefert `.page section + section` aus P1, P6 baut dafür keine eigene Regel. Die Reihenfolge der Abschnitte bleibt: links Antrag, Grund / Nachricht, Nachweise, Verlauf, rechts Kunde & Vertrag, Weitere Anträge, Automatische Prüfung, Prüfung durch dich, Berechnung bzw. Entscheidung.
|
||||
- **Ersetzungen:**
|
||||
- `dl.area-kv` wird `dl.kv`.
|
||||
- Mehrzeiler (`<br />` plus `area-muted`) werden `.cell-sub`.
|
||||
- Nachweise, Verlauf, Weitere Anträge und Prüfpunkte werden `.list-row`. Text aus `r.message` und Verlaufstexten ist `.prose`.
|
||||
- Alle Links (`domain`, Nachweise) bekommen `className="link"`. Die Nummer in „Weitere Anträge“ ist `<button type="button" className="link" onClick={() => onOpen?.(o.id)}>` statt `<a href="#">`.
|
||||
- Prüfpunkte tragen `<Badge tone={checkTone(k.status)}>{checkGlyph(k.status)}</Badge>`.
|
||||
- „In Stripe öffnen“ ist `Button as="a" …` ohne Icon, `KundenansichtButton` ohne `variant`.
|
||||
- **Formular:** Die manuellen Punkte stehen als `div.field` mit je einem `label.check`. „Berechnung“ besteht aus zwei `.field-row` mit `Field label htmlFor` (IDs `calc-amount`, `calc-months`, `calc-paid-until`, `calc-from`, Klassen `input`/`select`). `calcText` ist `p.strong`, der Hinweis `p.muted.small`. „Gutschrift (€)“ (`#credit-amount`) und das Nachrichtenfeld (`textarea#credit-note.textarea`, `maxLength={4000}`, langes Label wörtlich) sind `Field`.
|
||||
- **Knopfreihe** `div.flex.items-center.gap-3.wrap`:
|
||||
- `Button variant="primary"` „Genehmigen & gutschreiben“ (`disabled={!canApprove}`),
|
||||
- `Button` „Rückfrage senden“,
|
||||
- `Button variant="danger"` „Ablehnen“.
|
||||
- Bei `busyAction` zeigt der gedrückte Knopf „Speichert …“ (genehmigen, ablehnen) bzw. „Sendet …“ (Rückfrage).
|
||||
- „Jetzt in Stripe gutschreiben“ ist `variant="primary"` mit „Speichert …“ bei `apply`.
|
||||
- **Meldungen:** `actionError` als `p.msg.err[role=alert]`. Ladefehler `<Card><p className="msg err" role="alert">…</p><Button onClick={onClose}>Schließen</Button></Card>` (ohne Titel und Aktionen, also ohne `h2`). Laden `div.loading`.
|
||||
- `react-icons`, `admin-areas.css` und alle 9 Inline-Styles entfallen.
|
||||
|
||||
- [ ] **Step 3: Prüfen und Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t7" --only gutschriften-antrag,gutschriften-sperre --gate p6`
|
||||
Expected: Exit 0, die Sperr-Prüfung besteht (1 POST `…/decision`, 1 POST `…/calc`). `gutschriften-antrag` bei 1440 px zweispaltig mit genau einem schwarzen Knopf und „Ablehnen“ als rotem Text, bei 390 px einspaltig ohne Überlauf.
|
||||
|
||||
```bash
|
||||
git add src/components/credits/CreditDetail.jsx
|
||||
git commit -m "Design P6: Gutschrift-Detail zweispaltig mit Abschnitten und kv-Listen, Verhalten unveraendert" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: Abo-Angebote: Seite, Ansicht, Liste
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/OffersPage.jsx` (Z. 10), `src/components/offers/OffersView.jsx` (Z. 2, 14 bis 34), `src/components/offers/OfferList.jsx` (Z. 2, 7, 54 bis 118)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Tabs`, `Badge`, `Button`, `EmptyState`; Klassen wie Task 6; `filterOffers`, `offerRowActions`, `offerMailLine`.
|
||||
- Produces: `OfferList({ customerId, reloadKey, onMessage, onChanged })` und `OffersView({ customerId, customerName })` unverändert.
|
||||
|
||||
- [ ] **Step 1: `OfferList` umbauen**
|
||||
|
||||
- Tabs „Offen & aktiv“ (`open`) und „Alle“ (`all`), `shown = filterOffers(offers, filter)`. Laden, Fehler und Leerzustand wie bei den Gutschriften („Keine offenen oder aktiven Angebote.“ / „Noch keine Angebote.“).
|
||||
- `table.table.table-wrap` mit denselben Spalten und Zellinhalten wie heute, Mehrzeiler als `.cell-sub`. Klassen: Angelegt `hide-md`, Gültig bis `hide-sm`. Stripe-Links `a.link`.
|
||||
- Statuszelle: `Badge`, `.cell-sub` „seit …“ / „wartet auf Abschluss“, „Stripe: Coupon · Abo“, danach `offerMailLine(o)` als `.cell-sub` (`warn` als `span.msg.warn`).
|
||||
- Aktionen unter den Status-Zeilen, `div.flex.gap-3.wrap.mt-1`, nur wenn `sendLabel` oder `canWithdraw`:
|
||||
- `<button type="button" className="link" title="Angebots-E-Mail an … senden" onClick={() => sendMail(o)} disabled={busyId === o.id}>{sendLabel}</button>`
|
||||
- `Button variant="danger"` „Zurückziehen“
|
||||
- `withdraw` und `sendMail` bleiben unverändert (2 `confirm`, 2 `alert`).
|
||||
- Imports `FaSpinner`, `Card`, `admin-areas.css` und die drei Inline-Styles entfallen.
|
||||
|
||||
- [ ] **Step 2: `OffersView` und `OffersPage`**
|
||||
|
||||
`OffersView`: Meldung als `<p className="msg ok mb-2" role="status">`, dann `<div className="toolbar"><Button variant="primary" …>Neues Angebot</Button></div>` (ohne `FaPlus`, auch bei offenem Dialog sichtbar), dann `OfferList`, am Ende `{showForm && <OfferForm … />}` ohne Stil-`div`. `OffersPage`: Der Absatz „Ein Angebot legt sofort …“ bleibt wörtlich als `<p className="table-foot">`.
|
||||
|
||||
- [ ] **Step 3: Prüfen und Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t8" --only angebote,angebote-tablet,angebote-fehler,kunde-abo --gate p6`
|
||||
Expected: Exit 0. 390 px: Kunde, Angebot, Status, die 50-Zeichen-E-Mail bricht, „E-Mail senden“ und „Zurückziehen“ sind erreichbar. 1440 px: alle fünf Spalten.
|
||||
|
||||
```bash
|
||||
git add src/pages/OffersPage.jsx src/components/offers/OffersView.jsx src/components/offers/OfferList.jsx
|
||||
git commit -m "Design P6: Angebote-Liste als Tabelle mit Reitern, Aktionen als Textlinks" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 9: `OfferForm` als Dialog
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/offers/OfferForm.jsx` (Imports Z. 1 bis 8, Markup Z. 85 bis 143)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Dialog({ title, onClose, busy, footer, children })`, `Field`, `Button`, `AutocompleteInput` (Prop `id`), `.field-row`, `.check`, `.msg.err`.
|
||||
- Produces: `OfferForm({ fixedCustomerId, fixedCustomerName, onCreated, onCancel })` unverändert. `onCancel` ist zugleich `onClose` des Dialogs.
|
||||
|
||||
- [ ] **Step 1: Markup als Dialog**
|
||||
|
||||
Kontext-Effekt, `evaluation`, `save` (`window.confirm(offerConfirm(…))`), `suggestions` und `onPick` bleiben. Der Rückgabewert wird `<Dialog title="Neues Angebot" onClose={onCancel} busy={busy} footer={…}>` ohne `<form>`. Felder in der heutigen Reihenfolge und mit den heutigen Labeltexten, alle als `Field` mit neuen IDs `offer-*` für die Label-Bindung:
|
||||
- „Kunde“: mit `fixedCustomerId` `<p className="strong">{fixedCustomerName || 'Kunde'}</p>`, sonst `AutocompleteInput id="offer-customer"`.
|
||||
- `.field-row` „Produkt“ (`select.select`) und „Preis pro Monat im Angebot (€)“ (`input.input`).
|
||||
- `.field-row` „für“ (`select.select`) und „Angebot gültig bis“ (`input.input type="date"`).
|
||||
- „Hinweis für den Kunden …“ und „Interne Notiz …“ (`input.input`, `maxLength` 500 bzw. 2000).
|
||||
- `label.check` „Kunden per E-Mail über das Angebot informieren“.
|
||||
- Vorschau: Fehlerzeilen `p.msg.err`, Vorschauzeilen als `p`, die Platzhaltertexte wörtlich. Bei `error` `p.msg.err[role=alert]`.
|
||||
|
||||
`footer`: `Button variant="primary" type="button"` (`onClick={save}`, `disabled={!evaluation || evaluation.errors.length > 0 || busy}`) mit `busy ? 'Speichert …' : evaluation?.saveLabel || 'Angebot anlegen'` und `Button type="button"` „Abbrechen“ (`onClick={onCancel}`, `disabled={busy}`). `FaSpinner`, `Card`, `admin-areas.css`, die vier Inline-Styles und die Klasse `offer-form` entfallen.
|
||||
|
||||
- [ ] **Step 2: Clipping und Tastatur prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t9" --only angebote-dialog,angebote-vorschlag,angebote-laufend,angebote-tastatur,kunde-abo --gate p6`
|
||||
Expected: Exit 0, alle Schritt-Prüfungen bestehen. `angebote-vorschlag`: Liste vollständig erreichbar. `angebote-laufend` bei 390 px: Der lange Speichern-Text bricht in eine zweite Zeile der Fußzeile. `angebote-tastatur`: Esc schließt erst die Liste, dann den Dialog, beim Speichern wirken „ד und Esc nicht.
|
||||
|
||||
Besteht die Clipping-Prüfung nicht, bekommt `.dialog-body .autocomplete-list` im Block aus Task 3 `max-height: 200px` (nie `!important`, die Komponente bleibt unberührt).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/offers/OfferForm.jsx
|
||||
git commit -m "Design P6: Angebots-Formular als Dialog (Esc, Sperre beim Speichern, Vorschlagsliste geprueft)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
War die Abhilfe nötig, kommt `src/styles/global.css` ausdrücklich in dieses `git add`.
|
||||
|
||||
### Task 10: E-Mail-Speicher
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/StorageTasksPage.jsx` (Imports Z. 1 bis 8, Markup Z. 38 bis 98)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `PageHeader`, `Card`, `Badge`, `Button`, `EmptyState`; `.table.table-wrap`, `.hide-sm`, `.hide-md`, `.cell-sub`, `.link`, `.msg`, `.list-row`, `.table-foot`, `.loading`; `storageDue`.
|
||||
- Produces: Seite `/customers/email-speicher` unverändert.
|
||||
|
||||
- [ ] **Step 1: Markup umbauen**
|
||||
|
||||
- Fehler `div.flex.items-center.gap-2.wrap` mit `span.msg.err[role=alert]` und `Button` „Erneut versuchen“ (`onClick={load}`). Laden `div.loading` „Wird geladen …“.
|
||||
- Zusammenfassung `<p className={summary.warn ? 'msg warn mb-2' : 'muted mb-2'}>`.
|
||||
- Bei null Aufgaben `EmptyState` „Keine offenen Speicher-Aufgaben.“, sonst `table.table.table-wrap` mit den heutigen Spalten und Zellinhalten, Mehrzeiler als `.cell-sub`. Klassen: Fällig `hide-sm`, Bestellt `hide-md`.
|
||||
- Fällig-Zelle: `Badge tone={due.tone}` mit `due.label` und `due.sub` (`.cell-sub`, wenn `due.subMuted`). Kundenname `Link className="link"` auf `/customers/${t.customerId}`. Die Teilfehler-Zeile `partialText(t)` ist `span.msg.err`.
|
||||
- Letzte Zelle ohne Kopftext: `<button type="button" className="link" data-storage-done={t.id} onClick={() => done(t)} disabled={busyId === t.id}>{busyId === t.id ? 'Speichert …' : 'Erledigt'}</button>`.
|
||||
- „Gescheiterte Änderungen (letzte 30 Tage)“ bleibt `Card title`, die Zeilen sind `.list-row` (Text `failedText` als `span.muted`). Der lange Hinweisabsatz bleibt wörtlich als `<p className="table-foot">`.
|
||||
- `done`, `window.confirm('Stripe-Menge von Hand korrigiert?')` und `window.alert` bleiben. `react-icons`, `admin-areas.css` und beide Inline-Styles entfallen.
|
||||
|
||||
- [ ] **Step 2: Prüfen und Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t10" --only speicher,speicher-fehler --gate p6`
|
||||
Expected: Exit 0. 390 px: Kunde, Aufgabe und „Erledigt“ ohne Überlauf. 1440 px: alle fünf Spalten, Fällig als Badge mit Datum.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -c "data-storage-done" src/pages/StorageTasksPage.jsx`
|
||||
Expected: `1`.
|
||||
|
||||
```bash
|
||||
git add src/pages/StorageTasksPage.jsx
|
||||
git commit -m "Design P6: E-Mail-Speicher als Tabelle, Erledigt als Textlink mit data-storage-done" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 11: `admin-areas.css` löschen, KOMPAT-Reste
|
||||
|
||||
**Files:**
|
||||
- Delete: `src/styles/admin-areas.css`
|
||||
- Modify: `src/styles/global.css` (KOMPAT-Zeilen `.credit-cols a`, `.area-rows a`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `$W/css-klassen.mjs` aus P1. P4 hat die `.funktionen-*`-Regeln nach `global.css` verlegt, die Tasks 6 bis 10 haben die Importzeilen der P6-Dateien entfernt.
|
||||
- Produces: keine Datei `admin-areas.css`, keinen Import und keine Klasse aus ihr.
|
||||
|
||||
- [ ] **Step 1: Restnutzer ermitteln**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "\barea-|credit-(row|cols|files|checks|history|manual|calc|field)|offer-(row|form)|storage-row" src --glob '!*.css'; rg -n "admin-areas" src`
|
||||
Expected: keine Treffer, auch nicht in `CustomerFunktionen.jsx` und `AboStatusCard.jsx`. Ein Treffer in einer P6-Datei wird dort behoben. Ein Treffer in einer P4-Datei heißt, dass P4 sie nicht vollständig gelöst hat: stoppen und melden. Das Muster enthält bewusst kein `funktionen-`, denn diese Klassen leben in `global.css`.
|
||||
|
||||
- [ ] **Step 2: Datei löschen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/styles/admin-areas.css && rg -n "admin-areas" src`
|
||||
Expected: keine Treffer.
|
||||
|
||||
- [ ] **Step 3: KOMPAT-Reste löschen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "credit-cols|area-rows|side-count" src/styles`
|
||||
Expected: genau die beiden Zeilen in `global.css` (KOMPAT „Links in Bestandsklassen“) für `.credit-cols a` und `.area-rows a`. Beide werden gelöscht, `.side-count` ist schon seit Task 5 weg. Danach liefert derselbe Befehl keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.join('\n'))"`
|
||||
Expected: nur die in P1 erlaubten Namen (`p-4`, `btn-sm`, `status-filter`, `supporters-editor`, `timeline`, Bootstrap-Namen in `WorksheetList.jsx`). Meldet das Skript die gelöschte Datei als fehlend, wird sie in der Dateiliste von `css-klassen.mjs` (unter `$W`) gestrichen.
|
||||
|
||||
- [ ] **Step 4: Bauen und Commit**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6-t11" --only kunde-abo,kunde-gutschriften,kunde-funktionen,gutschriften,angebote,speicher --gate p6`
|
||||
Expected: Exit 0. Die Kundenseiten-Reiter `kunde-abo` und `kunde-funktionen` sehen aus wie nach P4 (Vergleich mit `$W/shots/p4`), die drei Listen wie nach Task 6, 8 und 10.
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git add src/styles/global.css
|
||||
git commit -m "Design P6: admin-areas.css geloescht, KOMPAT-Reste entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
`git status --short` vor dem Commit darf nur `D src/styles/admin-areas.css` (vom `git rm` schon vorgemerkt) und `M src/styles/global.css` zeigen. Die Löschung steht damit im selben Commit.
|
||||
|
||||
### Task 12: Gate P6, Review, Tag, Übergabe
|
||||
|
||||
- [ ] **Step 1: Rebase und Ausgangswerte**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0, `# fail 0`, `# pass` = N5 + 15 (10 Lib-Tests, 5 `chatCss`). Liegen neue Commits auf `test`, ist die Zahl entsprechend höher und wird notiert. Die Bundle-Größen aus der Ausgabe werden mit P5 verglichen, das CSS sollte kleiner sein (`admin-areas.css` ist weg).
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p6 && "$W/shots.sh" p6 "$W/shots/p6" --gate p6`
|
||||
Expected: Exit 0, in allen Ansichten kein Verstoß gegen `dark-surface`, `low-contrast`, `overflow-x`, `console-error`, `external-request`, `step-failed`. In den P6-Ansichten dazu kein `icon-svg` außerhalb von `.woms-chat`.
|
||||
|
||||
- [ ] **Step 3: Inventur und Hygiene**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && for f in src/components/credits/CreditDetail.jsx src/components/offers/OfferList.jsx src/components/offers/OfferForm.jsx src/pages/StorageTasksPage.jsx; do echo "$f $(rg -o 'window\.confirm|window\.alert|busyRef|data-storage-done|id="(calc|credit)-[a-z-]+"' $f | wc -l)"; done
|
||||
rg -n "react-icons|size=\"sm\"|style=\{\{|!important" src/pages/{CustomerChat,Credits,Offers,StorageTasks}Page.jsx src/components/{chat,credits,offers} src/styles/chat.css
|
||||
rg -n "admin-areas" src
|
||||
git diff --stat design-p5..HEAD -- src/lib/loadChatComposer.js tests/unit/loadChatComposer.test.mjs
|
||||
test ! -e src/styles/admin-areas.css && echo "admin-areas.css weg"
|
||||
```
|
||||
Expected: Zählung 16, 4, 1, 3. Die Befehle zwei bis vier sind leer, am Ende steht „admin-areas.css weg“.
|
||||
|
||||
- [ ] **Step 4: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p5..HEAD` gegen die Spec-Abschnitte „Bausteine“, „Seiten ohne Prototyp-Vorlage“ und P6. Schwerpunkte:
|
||||
- Jede Chat-Regel hat Präfix und strikt höhere Spezifität (Tabelle in Task 4).
|
||||
- `admin-areas.css` ist gelöscht, kein Import und keine ihrer Klassen ist übrig, `.funktionen-*` stehen unverändert in `global.css`.
|
||||
- `CreditDetail` verhält sich unverändert (`busyRef`, Debounce, `confirm`).
|
||||
- Der Textdiff (`git diff design-p5..HEAD -w`) zeigt nur Textänderungen, die die Spec unter „Texte“ erlaubt (darunter „Speichert …“, „Sendet …“, „Wird geladen …“) oder die ein Icon durch ein Zeichen ersetzen (z. B. „← Alle Unterhaltungen“).
|
||||
- Jede Aktion der alten Seiten ist erreichbar: Prüfen/Ansehen, Schließen, Kundenansicht öffnen, In Stripe öffnen, Weitere Anträge, Genehmigen, Rückfrage, Ablehnen, Jetzt in Stripe gutschreiben, Neues Angebot, E-Mail senden/Erneut senden, Zurückziehen, Coupon- und Abo-Link, Erledigt, Kundenlink, Erneut versuchen, Neue Nachricht an Kunde, Unterhaltung öffnen, Zurück.
|
||||
|
||||
Wichtige Befunde werden behoben. Danach kommt der Rebase aus Step 1 noch einmal und Step 2 als Kurzlauf mit `--only chat-verlauf,gutschriften,angebote,speicher`.
|
||||
|
||||
- [ ] **Step 5: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p6 && git log --oneline origin/test..design-p6 && git rev-parse design-p6`
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- die Screenshots aus `shots/p6/1440x900` und `shots/p6/390x844`. Auswahl: `chat`, `chat-verlauf`, `chat-menue`, `chat-anhang-fehler`, `chat-tablet`, `gutschriften`, `gutschriften-antrag`, `gutschriften-tablet`, `angebote`, `angebote-dialog`, `angebote-vorschlag`, `speicher`, `kunde-chat`, `kunde-abo`.
|
||||
- die bewusst offenen Punkte:
|
||||
- Lightbox und Kamera bleiben dunkel, „Foto verwenden“ bleibt im Portal-Lila (nicht aus WOMS änderbar).
|
||||
- Die Anhang-Menü-Icons des Portal-Bausteins bleiben.
|
||||
- Die Blasenfarben `--bg-hover` und `--grey-bg` sind auf Weiß sehr zart. Die Stellschrauben sind `--chat-bubble-in` und `--chat-bubble-out` in `chat.css`.
|
||||
- den Hinweis zu tp2: `admin-areas.css` ist gelöscht. Ändert `feature/terminplaner-tp2` die Datei (Anleitungs-Panel der Funktionen), gibt es beim Zusammenführen einen Konflikt, und die Panel-Regeln gehören dann in den Block „Kunden (P4)“ von `global.css`.
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p6^{commit}:refs/heads/test'`
|
||||
- die Rücknahme `git revert --no-commit design-p5..design-p6` als Commit auf `test`, durch den Eigentümer. Sie gilt nur, solange P6 das jüngste Paket ist.
|
||||
|
||||
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` stimmt mit `dist/index.html` des Tags überein.
|
||||
- Lesender Sichttest im Chrome des Eigentümers: `/customers/chat` mit geöffnetem Verlauf (Anhang-Menü öffnen), `/customers/gutschriften` und `/customers/angebote`.
|
||||
@@ -0,0 +1,537 @@
|
||||
# WOMS „Weißes Design“ P7: Finanzen – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P7 sind alle Finanzseiten ohne Karten, Kästen und Icons gestaltet: Übersicht, Einstellungen, Bankrückkehr und der Reiter „Rechnungen“ am Kunden. Die Dialoge „Buchung zuordnen“ und „Rechnung schreiben“ laufen auf `Dialog`. `Kpi` ist gelöscht. Funktion, Rückfragen und Texte bleiben.
|
||||
|
||||
**Architecture:**
|
||||
- Der Finanzblock in `src/styles/global.css` (heute ab `/* --- Finanzen --- */`, ca. 320 Zeilen) wird Region für Region neu geschrieben, jeweils zusammen mit den Komponenten der Region. Er enthält nur Tokens, keine Farbliterale und kein `!important`. Task 3 rahmt ihn mit zwei Marken, weil P5 und P6 hinter ihm eigene Blöcke anhängen.
|
||||
- Die Container-Queries (560 Vorschau, 640 Buchungen, 860 Rechnungen) bleiben. `container-type` sitzt auf einem neuen Wrapper `.fin-body` unter der Abschnittsüberschrift, Dialoge stehen nie darin.
|
||||
- Neue Datei `src/lib/financeUi.js` mit zwei reinen Helfern: Vorzeichen-Rot und Meldungsklassen.
|
||||
|
||||
**Tech Stack:** React 18, react-router-dom 6, Vite 5, reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Abschnitt, Knöpfe, Badge, Tabelle, Schlüsselwerte, Formular, Dialog, Meldungen, Laden), „Bewusste Änderungen“ (Esc), „Risiken“ (Scroll-Anker, Vorschlagslisten in Dialogen) und Paket P7. Dazu Anhang `…-umbaukarte.md`, Abschnitte 3, 4, 5/P7, 7.8. Globale Regeln und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P7 kommt hinzu:
|
||||
|
||||
- **Befehle:** Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh &&`. Danach gelten `$WT`, `$W` und `$TMPDIR`. Tests und Build laufen nur über `"$W/check.sh"`, Browser-Läufe nur über `"$W/shots.sh"`. `FIN` ist die kommagetrennte Liste aller Ansichten-IDs der Tabelle aus Task 2 Step 2 (Shell-Variable, gesetzt in Task 2 Step 4).
|
||||
- **Gate-Läufe:** `--gate p7` schaltet `icon-svg` für die `FIN`-Ansichten scharf. Die Finanzseite ist erst nach Task 5 ohne Icons, deshalb laufen Task 3 und 4 mit `--gate p6` (dieselben übrigen Prüfungen). Ab Task 5 gilt `--gate p7`.
|
||||
- **Dateien:** nur `src/pages/FinancePage.jsx`, `FinanceSettingsPage.jsx`, `BankCallbackPage.jsx`, alle 17 Dateien in `src/components/finance/`, `src/components/ui/Kpi.jsx` (löschen), `src/components/ui/index.js` (Export entfernen), `src/lib/financeUi.js` (neu), `src/styles/global.css` (nur der Finanzblock). Alle `src/lib/finance*.js`, `src/hooks/*`, `CustomerDetailPage.jsx` und alle bestehenden Tests bleiben unberührt.
|
||||
- **Texte wörtlich.** Erlaubt sind nur `▾`/`▴` statt Chevron-Icons, „+ Position“ (Plus-Icon wird Zeichen), „Entfernen“ in der Positionszeile (heute nur Icon) und „← Zurück zu den Finanzen“. Ladetexte („Speichere …“, „Sende …“, „Rechnungen werden geladen …“) bleiben, nur Spinner und Icons entfallen.
|
||||
- **Tonnamen** `muted|info|ok|warn|danger` aus `financeFormat.js`/`financeView.js` bleiben, gemappt wird nur in `Badge`.
|
||||
- **Beträge** behalten das Vorzeichen (U+2212). Rot (`--red-fg`) gilt genau dann, wenn der angezeigte Text mit „−“ beginnt: Klasse `.fin-neg`, bei Kennzahlen `tone="neg"`. Wo die Lib schon markiert (`saldoSicherNegative`, `saldoErwartetNegative`, `low.negative`, `shortfall`), gilt diese Markierung weiter.
|
||||
- **Container-Falle:** `container-type: inline-size` macht das Element zum Containing Block für `position: fixed`. Es steht nur auf `.fin-body`, `AssignDialog` und `InvoiceCreateModal` sind nie Nachfahren davon.
|
||||
- **Hygiene in allen P7-Dateien:** keine Icon-Importe, kein `style={{…}}`, kein `size="sm"`, keine Farbliterale, keine Klassen `ui-btn`, `form-control`, `form-group`, `form-label`, `overlay-content`, `overlay-close`, `spinner`, `kpi`. `lockRef`/`busyRef` und alle `window.confirm`-Rückfragen bleiben.
|
||||
|
||||
**Gemeinsame Ersetzungen** (Komponenten und CSS):
|
||||
|
||||
| Alt | Neu |
|
||||
|---|---|
|
||||
| Spinner plus Text im `.empty` | `<div className="loading" role="status">Text wörtlich</div>` |
|
||||
| `fin-hint` / `fin-hint-warn` / `fin-dialog-*` | `msg muted` / `msg warn` (`role="alert"` bleibt) / Fehler im Dialog `msg err` |
|
||||
| tonabhängige Meldung | `<p {...noticeProps(tone, 'fin-notice')}>` |
|
||||
| `Button variant="ghost"`, `size="sm"` | `Button` (Standard), `size` entfällt |
|
||||
| Aktion in Zeile, Liste oder Meldung | `<button type="button" className="link">`, Links `<a className="link">`, destruktiv `Button variant="danger"` |
|
||||
| `form-control` / `form-group` + `form-label` | `input`/`select`/`textarea` / `Field` bzw. `FormField` |
|
||||
| CSS-Farben und -Maße | `--text-muted`, `--text-faint` → `--text-2`; `--warn` → `--orange-fg`; Text-`--danger` → `--red-fg` (Formularfehler bleibt `--danger`); `--border` → `--line`; Flächen und Kartenrahmen entfallen; Gewicht 700/800 → 600 (Tabellentitel 500); Großbuchstaben entfallen; Media-Queries 640/700/720 → 860 |
|
||||
|
||||
Die CSS-Schritte nennen nur Abweichungen, Spezifitäts- und Reihenfolgeregeln sowie Maße, die ein Gate-Schritt prüft. Werte, die dort fehlen, bleiben wie im heutigen Finanzblock und folgen der Ersetzungstabelle.
|
||||
|
||||
**Steuerelemente, die erreichbar bleiben** (Namen unverändert):
|
||||
- Übersicht: „Einstellungen“, „Aktualisieren“, „Aufschlüsselung“, der eine Knopf je Aufgabe (bank-verbinden, bank-aktualisieren, rechnung-oeffnen, zuordnen, kunde-oeffnen, fixkosten-uebernehmen, einstellungen), „Alle N anzeigen“/„Weniger anzeigen“, „Schließen“ (Zuordnen-Hinweis), jedes „Erneut versuchen“, das heute steht. Rechnungen: „Rechnung schreiben“, Filter Offen/Überfällig/Bezahlt/Entwürfe/Alle, Aktionen Zahlungsseite, PDF, Senden, Erinnerung senden, Stornieren, Löschen. Buchungen: „Zuordnen“/„Ändern“. Pager: „Zurück“/„Weiter“.
|
||||
- Dialoge: „ד, „Abbrechen“, „Schließen“, „Zuordnen“, „Zuordnung lösen“, „Zurück“, „Ja, als bezahlt markieren“, „Als Entwurf speichern“, „Senden“, „+ Position“, „Entfernen“, „Zur Übersicht“.
|
||||
- Einstellungen und Rest: „Speichern“, „Jetzt abrufen“, „Neu verbinden“/„Bank verbinden“, „Neue Position“, „Bearbeiten“, „Aktivieren“/„Auf inaktiv setzen“, „Löschen“, „Abbrechen“, „Zu den Finanzen“, „Daten erfassen“, „← Zurück zu den Finanzen“.
|
||||
|
||||
**Entscheidungen** (die Spec schweigt; nächste Lösung zu heute):
|
||||
- Entscheidung: `.fin-stack` ist ein normaler Block ohne `gap` und ohne Flex. Der Abstand der Seitenteile ist `margin-top: 36px` auf jedem Geschwister nach dem ersten (`.fin-stack > * + *`). Das ist derselbe Wert wie `.page section + section` aus P1. Beide Regeln setzen dieselbe Eigenschaft am selben Element, also addiert sich nichts. Ein `gap: 36px` ergäbe zwischen zwei Abschnitten 72 px. Als Block lässt `.fin-stack` den oberen Rand von `h2.section:first-child` (8 px, weil `Card` die Überschrift als erstes Kind in ein `<section>` setzt) mit dem Abschnittsabstand verschmelzen. Eine Sonderregel für die Überschrift entfällt.
|
||||
- Entscheidung: Finanztabellen sind keine `tr.row` (Zeilen nicht klickbar, Aktionen sind echte Knöpfe und Links). Es gibt kein `.hide-sm`/`.hide-md`, schmal stapeln die Container-Queries die Zeilen wie heute.
|
||||
- Entscheidung: Die Aufgaben-Aktion je Zeile und „Alle N anzeigen“ sind `.link` (bis zu sieben Ringknöpfe untereinander wären zu unruhig). „Aktualisieren“, allein stehendes „Erneut versuchen“, „Jetzt abrufen“ und „Neue Position“ sind Standardknöpfe.
|
||||
- Entscheidung: Pro Abschnitt höchstens ein schwarzer Knopf (Rechnung schreiben, Speichern, Jetzt abrufen bzw. Bank verbinden, Zuordnen/Senden im Dialog). Die Seitenköpfe haben keinen.
|
||||
- Entscheidung: Die Aufschlüsselungszeilen („− Steuer-Rücklage …“) bleiben neutral, weil sie Abzüge sind. Rot sind Summen, Salden sowie Posten- und Buchungsbeträge.
|
||||
- Entscheidung: Der Altersstreifen ist `<ul class="stats fin-ages">` mit `Stat` je Gruppe. Er überschreibt das Flex-Layout durch 4 Spalten, ab Container ≤ 860 px 2 Spalten (die Handy-Regel von `.stats` ergäbe 3 + 1).
|
||||
- Entscheidung: Der Dialog-Fuß ist `<div className="fin-foot">` (Flex, umbrechend, rechtsbündig, auf dem Handy gleich breite Knöpfe) statt `<span className="grow" />`. Mit `.dialog-foot { flex-wrap: wrap }` aus P6 allein stünde der dritte Knopf bei 390 px linksbündig in einer zweiten Zeile.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Aus der Übersicht gehören diese Punkte zu P7:
|
||||
- **Punkt 2, lange Inhalte am Handy:** Testdaten `finanz-minus` mit Kundenname (60 Zeichen), Verwendungszweck (140 Zeichen ohne Leerzeichen) und Gegenpartei (80 Zeichen). `overflow-x` ist im Gate `p7` für alle Ansichten scharf.
|
||||
- **Punkt 3, Teilausfall:** `finanzen-teilausfall` (Stripe aus, Abos nicht ladbar, Rechnungen und Buchungen antworten 500). Jeder Block zeigt seine Meldung mit „Erneut versuchen“, die Seite bleibt bedienbar.
|
||||
- **Punkt 4, nur Tastatur:** Esc, Tab-Falle, gesperrtes „ד bei `busy`, „Enter sendet nie“ und „Esc schließt zuerst die Vorschlagsliste“ sind Schritte in `dlg-zuordnen-*` und `dlg-rechnung-*`.
|
||||
- **Punkt 5, viele Einträge:** 120 Buchungen, 60 Rechnungen, 50 Zeilen je Seite (`finanzen-pager`).
|
||||
|
||||
Zusätzlich für P7 (Risiken der Spec): Scroll-Anker `finanzen-rechnungen` und `finanzen-buchungen` liegen nicht unter der Mobil-Leiste (`finanzen-anker`). Die Vorschlagsliste im Kundenfeld wird im scrollenden `.dialog-body` nicht abgeschnitten (`dlg-rechnung-kunde`). Negative Beträge sind rot und tragen ihr Minus (`finanzen-minus`, `finanzen-tabellen`).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Hilfsfunktionen `financeUi`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/financeUi.js`
|
||||
- Test: `tests/unit/financeUi.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `euro`, `euroWhole`, `signedEuro` aus `src/lib/financeFormat.js` (nur im Test).
|
||||
- Produces:
|
||||
- `isNegText(text) → boolean`: wahr, wenn `text` ein String ist und mit „−“ (U+2212) beginnt.
|
||||
- `noticeProps(tone, extra = '') → { className, role }`: `'ok'` → `msg ok`/`status`, `'warn'` → `msg warn`/`alert`, sonst `msg muted`/`status`. `extra` hängt mit Leerzeichen an `className`.
|
||||
|
||||
- [ ] **Step 1: Paketstart und Ausgangslage**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p6 HEAD && git rebase origin/test && git status --short && "$W/check.sh"`
|
||||
Expected: Der Rebase läuft ohne Konflikt, `git status` ist leer, `# fail 0`. Ist `design-p6` kein Vorfahr von `HEAD`, stoppt der Agent und meldet das. Testzahl N sowie JS- und CSS-Größe aus der `check.sh`-Ausgabe werden notiert (Vergleich in Task 9).
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/FinancePage.jsx src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance src/components/ui/Kpi.jsx src/components/ui/index.js src/lib/financeUi.js src/styles/global.css`
|
||||
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent und fragt den Eigentümer. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
|
||||
|
||||
- [ ] **Step 2: Failing Test schreiben**
|
||||
|
||||
```js
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const { isNegText, noticeProps } = await loadModule('src/lib/financeUi.js')
|
||||
const { euro, euroWhole, signedEuro } = await loadModule('src/lib/financeFormat.js')
|
||||
|
||||
test('isNegText: nur das angezeigte Minus zählt', () => {
|
||||
assert.equal(isNegText(euro(-449)), true)
|
||||
assert.equal(isNegText(signedEuro(-449)), true)
|
||||
assert.equal(isNegText(euroWhole(-123456)), true)
|
||||
assert.equal(isNegText(euroWhole(-40)), false) // „0 €“: Rundung auf 0 trägt kein Vorzeichen
|
||||
assert.equal(isNegText(signedEuro(16879)), false)
|
||||
assert.equal(isNegText(euro(null)), false) // „–“ (U+2013)
|
||||
for (const v of ['-4,49 €', '', undefined, null, 5]) assert.equal(isNegText(v), false)
|
||||
})
|
||||
|
||||
test('noticeProps: Klasse und Rolle je Ton', () => {
|
||||
assert.deepEqual(noticeProps('ok'), { className: 'msg ok', role: 'status' })
|
||||
assert.deepEqual(noticeProps('warn'), { className: 'msg warn', role: 'alert' })
|
||||
assert.deepEqual(noticeProps('info'), { className: 'msg muted', role: 'status' })
|
||||
assert.deepEqual(noticeProps(undefined), { className: 'msg muted', role: 'status' })
|
||||
assert.deepEqual(noticeProps('warn', 'fin-notice'), { className: 'msg warn fin-notice', role: 'alert' })
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/financeUi.test.mjs`
|
||||
Expected: FAIL (Modul `financeUi.js` fehlt).
|
||||
|
||||
- [ ] **Step 4: `financeUi.js` implementieren**
|
||||
|
||||
Zwei benannte Exporte nach der Signatur oben, deutscher Kommentar im Stil der Nachbardateien, keine Abhängigkeit.
|
||||
|
||||
- [ ] **Step 5: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/financeUi.test.mjs && "$W/check.sh" --nur-tests`
|
||||
Expected: PASS, `# pass` = N + 2, `# fail 0`, Exit 0.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/financeUi.js tests/unit/financeUi.test.mjs
|
||||
git commit -m "Finanzen: Helfer fuer Vorzeichen-Rot und Meldungsklassen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 2: Werkzeug – Finanzdaten, Ansichten, Gate `p7`
|
||||
|
||||
**Files** (unter `$W/`):
|
||||
- Create: `fixtures/finanz-minus.mjs`, `fixtures/finanz-teilausfall.mjs`
|
||||
- Modify: `views.mjs`, `gates.mjs`, bei Bedarf `backend.mjs` (`POST …/finanzen/buchungen/:id/zuordnen` → `{ buchung }`, `POST …/finanzen/bank/rueckkehr` → `{ ok: true, validUntil: '2027-03-29' }`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `VIEWS`- und `GATES`-Schema aus der Übersicht (`id`, `path`, `fixtures`, `scenario`, `steps(page, ctx)`). Schritte werfen bei Abweichung (`node:assert/strict`), `shots.mjs` meldet das als `step-failed`. `ctx.backend.calls` listet alle beantworteten Requests. Portal-Antworten unter `https://portal.test/api/admin/finanzen/…`.
|
||||
- Produces: die Ansichten der Tabelle unten und das Gate `p7` (Aufbau nach der Übersicht, Step 3).
|
||||
|
||||
- [ ] **Step 1: Testdaten anlegen**
|
||||
|
||||
Beide Dateien leiten von `standard.mjs` ab und ersetzen nur die Finanz-Antworten (gleiche Form).
|
||||
- `finanz-minus`:
|
||||
- Kopf: Kontostand −1.234,56 €, `verfuegbarCents` < 0, `tiefsterSicher` negativ, `tiefsterErwartet` positiv (beweist Rot je Betrag).
|
||||
- Vorschau mit 8 Posten und negativen Salden. 9 Aufgaben, darunter je eine mit `rechnung-oeffnen` ohne `hostedUrl`, `zuordnen`, `bank-aktualisieren`, `fixkosten-uebernehmen`.
|
||||
- Ein Abo `past_due` (gefährdet), alle 4 Altersgruppen mit Werten.
|
||||
- 60 Rechnungen aller Status (eine Abo-Rechnung, ein Kundenname mit 60 Zeichen). 120 Buchungen: Ein- und Ausgänge, `PDNG`, Verwendungszweck mit 140 Zeichen ohne Leerzeichen, Gegenpartei mit 80 Zeichen, offene Eingänge mit `vorschlaege` (Rechnung und Auszahlung).
|
||||
- Ein fester Kunde mit E-Mail, aber ohne Straße, PLZ und Ort.
|
||||
- `finanz-teilausfall`: `stand.stripe = { ok: false, fehler: 'Stripe ist gerade nicht erreichbar.' }`, `abos = { fehler: 'Die Abos konnten nicht geladen werden.' }`, Kopf ohne Kontostand.
|
||||
|
||||
- [ ] **Step 2: Ansichten anlegen oder anpassen**
|
||||
|
||||
| ID | Aufbau | Prüfung (Schritt wirft bei Abweichung) |
|
||||
|---|---|---|
|
||||
| `finanzen` (vorhanden) | `/finance` | Alle Namen aus „Steuerelemente, die erreichbar bleiben“ (Global Constraints) per `getByRole` auffindbar. Mindestens ein `button.link`, alle mit transparentem `background-color` und `border-top-width: 0px` (so auch jedes `button.link-danger` in `finanzen-tabellen`). |
|
||||
| `finanzen-minus` | `finanz-minus`; Klick „Aufschlüsselung“ | Text in `.stat .v` bzw. `.fin-low .fin-amount`, der mit „−“ beginnt, trägt `.v.neg` bzw. `.fin-neg`. Kein `.v.neg` ohne „−“ (außer in `.fin-abo-stats`). Kein `.fin-breakdown li` mit `.fin-neg`. |
|
||||
| `finanzen-tabellen` | `finanz-minus` | `.fin-forecast-amount`, `.fin-tx-amount` mit „−“ tragen `.fin-neg`, die anderen nicht. `thead` von `.fin-forecast`, `.fin-invoices`, `.fin-tx` ist bei 1440 nicht `position: absolute`, bei 390 `absolute`. Dreimal `.fin-body` mit `container-type: inline-size`, kein `.overlay` darin. Zwischen `#finanzen-rechnungen` und `#finanzen-buchungen` liegen 36 px (± 1 px), nicht 72. Bei 1440 liegen die Oberkanten der beiden Abschnitte in `.fin-cols` gleich hoch (± 1 px). |
|
||||
| `finanzen-pager` | `finanz-minus`, zu `#finanzen-buchungen`, „Weiter“ | Fuß „Seite 2 von 3 · 120 Buchungen“, 50 `.fin-tx tbody tr`. |
|
||||
| `finanzen-anker` | `finanz-minus`, Klick „Zu den Rechnungen“, 800 ms | `#finanzen-rechnungen` hat `scroll-margin-top` 16px (1440) bzw. 68px (390), seine Oberkante liegt dort (± 1 px). |
|
||||
| `finanzen-teilausfall` | `finanz-teilausfall`, Szenario `fehler`, `/rechnungen` und `/buchungen` → 500 (`page.route`), Reload | Mindestens 2 `[role=alert]` und 2 „Erneut versuchen“, h1 „Finanzen“ sichtbar, „Aktualisieren“ nicht `disabled`. |
|
||||
| `finanzen-fehler` | Szenario `fehler`, `/uebersicht` → 500, Reload | `[role=alert]` und klickbares „Erneut versuchen“. |
|
||||
| `dlg-rechnung` (vorhanden) | Klick „Rechnung schreiben“ | `role=dialog` mit Name „Rechnung schreiben“, Klasse `dialog-wide`. Overlay-Rechteck so groß wie das Fenster, kein Vorfahr `.fin-body`. Fokus im Dialog. |
|
||||
| `dlg-rechnung-kunde` | „Mü“ in `#fin-invoice-customer` | Feldbreite ≥ 90 % von `.dialog-body`. `[role=listbox]` bei 1440 ganz im `.dialog`, bei 390 ≥ 120 px sichtbar. Esc schließt nur die Liste, das zweite Esc den Dialog. |
|
||||
| `dlg-rechnung-enter` | Fokus in Beschreibung, Enter; `ctx.backend.calls` zählt `POST …/rechnungen` | Dialog bleibt, 0 Requests. „Als Entwurf speichern“ ohne Kunde zeigt `.msg.err[role=alert]` im sichtbaren Teil von `.dialog-body`. |
|
||||
| `dlg-zuordnen-zahlung` (vorhanden) | Klick „Zuordnen“ an erster offener Eingangsbuchung | Vorschläge sind `label.check` mit Radio. 12 × Tab und 12 × Umschalt+Tab bleiben im `.dialog`. |
|
||||
| `dlg-zuordnen-bestaetigung` | Vorschlag mit Rechnung, „Zuordnen“ | `.fin-confirm` nennt „in Stripe als bezahlt markiert“, Fuß „Zurück“ und „Ja, als bezahlt markieren“, `ctx.backend.calls` enthält kein `POST …/zuordnen`. Esc schließt ohne Request. |
|
||||
| `dlg-zuordnen-busy` | wie oben, `POST …/zuordnen` 1500 ms verzögert, „Ja, …“ | Währenddessen `.dialog-close` `disabled`, Esc lässt den Dialog offen. Danach schließt er. |
|
||||
| `finanzen-einstellungen` (vorhanden) | `/finance/einstellungen` | h2 „Einstellungen“, „Bankzugang“, „Fixkosten“, Link „← Zurück zu den Finanzen“. |
|
||||
| `finanzen-einstellungen-fehler` | Mindestpuffer „abc“, „Speichern“, dann „Neue Position“ | `[aria-invalid="true"]` hat `box-shadow` mit `rgba(192, 18, 60`, `.msg.err[role=alert]` am Feld. h3 „Neue Position“, Fokus in `#fin-fix-name`. |
|
||||
| `finanzen-bank-callback`, `-ok` | `/finanzen/bank-callback`, bzw. mit `?code=x&state=y` | „Zu dieser Seite liegt keine Rückmeldung der Bank vor.“ bzw. `.msg.ok` mit „Bankzugang erneuert, gültig bis“. |
|
||||
| `kunde-invoices` (vorhanden), `kunde-invoices-adresse` | Kunde ohne Anschrift aus `finanz-minus` (andere URL als `kunde-invoices`) | `.fin-address-warn-page` mit Rolle alert und „Zur Übersicht“. „Senden“ eines Entwurfs über 250 € ist `disabled`. |
|
||||
|
||||
- [ ] **Step 3: Gate-Eintrag**
|
||||
|
||||
Ein Eintrag nach dem Schema der Übersicht. `p7` ist `p6` plus `icon-svg` für die `FIN`-Ansichten und die aller früheren Pakete. Eine `ignore`-Liste des Vorgängers bleibt unverändert.
|
||||
|
||||
```js
|
||||
p7: {
|
||||
...GATES.p6,
|
||||
scoped: {
|
||||
...GATES.p6.scoped,
|
||||
'icon-svg': { ...GATES.p6.scoped['icon-svg'], views: [...GATES.p6.scoped['icon-svg'].views, /* IDs aus FIN */] },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Die absichtlichen 500er der zwei Fehleransichten schreibt der Browser als `console.error` („Failed to load resource“). Deshalb laufen sie im Szenario `fehler`, dessen Meldungen `console-error` ausnimmt. Nimmt `checks.mjs` nur Meldungen der eigenen Fehlerantworten aus, wird die Ausnahme um Meldungen zu `…/api/admin/finanzen/` erweitert.
|
||||
|
||||
- [ ] **Step 4: Vorher-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && FIN=finanzen,finanzen-minus,finanzen-tabellen,finanzen-pager,finanzen-anker,finanzen-teilausfall,finanzen-fehler,finanzen-einstellungen,finanzen-einstellungen-fehler,finanzen-bank-callback,finanzen-bank-callback-ok,dlg-rechnung,dlg-rechnung-kunde,dlg-rechnung-enter,dlg-zuordnen-zahlung,dlg-zuordnen-bestaetigung,dlg-zuordnen-busy,kunde-invoices,kunde-invoices-adresse && "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-vorher" --only "$FIN" --gate p7`
|
||||
Expected: Exit 1. `icon-svg` meldet `finanzen`, `finanzen-einstellungen`, `dlg-rechnung`, `dlg-zuordnen-zahlung`. Die Schritte schlagen an in `finanzen`, `finanzen-minus`, `finanzen-tabellen`, `finanzen-anker` (390 px), `finanzen-einstellungen-fehler`, `dlg-rechnung*` und `dlg-zuordnen-zahlung`. `finanzen-pager`, `finanzen-teilausfall`, `finanzen-fehler`, `dlg-zuordnen-bestaetigung` und `dlg-zuordnen-busy` sind Wächter für heutiges Verhalten und laufen schon vorher durch.
|
||||
|
||||
### Task 3: Seite, Statuszeile, Liquiditätskopf, Aufgaben; `Kpi` löschen
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/FinancePage.jsx`, `src/components/finance/StatusLine.jsx`, `LiquidityHead.jsx`, `TodoList.jsx`, `src/components/ui/index.js`
|
||||
- Delete: `src/components/ui/Kpi.jsx`
|
||||
- Modify: `src/styles/global.css` (Marken um den Finanzblock, Regionen „Allgemein“ bis `Block 2: Zu erledigen`, `.kpi*`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Stat({ label, value, sub, tone })`, `Card`, `Badge`, `EmptyState`, `PageHeader`, `Button` (P1), `isNegText`, `noticeProps` (Task 1).
|
||||
- Produces: die Klassen `.fin-stack`, `.fin-top`, `.fin-block`, `.fin-body`, `.fin-table-wrap`, `.fin-notice`, `.fin-load-error`, `.fin-cols`, `.fin-neg`, `.fin-flag`, `.fin-amount`, `.fin-sr` für Task 4 bis 8.
|
||||
|
||||
- [ ] **Step 1: P1-Voraussetzungen prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "Stat|Dialog" src/components/ui/index.js && rg -n "\.stat \.sub|\.table-foot|\.msg\b|\.loading|\.field-row\b" src/styles/global.css | head`
|
||||
Expected: `Stat` und `Dialog` sind exportiert, die Klassen definiert. Fehlt ein Export, wird er in `index.js` ergänzt.
|
||||
|
||||
- [ ] **Step 2: Finanzblock rahmen, CSS-Region „Allgemein“**
|
||||
|
||||
Zuerst die Grenzen setzen.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "^/\* (--- Finanzen|Finanzen: Einstellungen|=====|Kunden-Unterseiten|Projekte \(P5\))" src/styles/global.css`
|
||||
Expected: Der Finanzblock beginnt bei `/* --- Finanzen --- */` und endet vor dem nächsten Blockkommentar (P6, KOMPAT …) bzw. am Dateiende. Davor kommt die Marke `/* ===== FINANZEN (P7) ===== */`, dahinter `/* ===== ENDE FINANZEN ===== */`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n -A2 "===== FINANZEN \(P7\)" src/styles/global.css`
|
||||
Expected: die Marke, danach `.fin-amount`.
|
||||
|
||||
`.fin-amount` und `.fin-sr` bleiben (Werte gleich). Neu oder geändert:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.fin-neg`, `.fin-flag` | `color: var(--red-fg)`, `.fin-flag` zusätzlich `font-weight: 600` |
|
||||
| `.fin-stack` | keine eigene Regel (Block, kein `gap`, siehe Entscheidung oben) |
|
||||
| `.fin-stack > * + *` | `margin-top: 36px` |
|
||||
| `.fin-top` | `display: flex; flex-direction: column; gap: 20px` |
|
||||
| `.fin-block` | `scroll-margin-top: 16px`, bei ≤ 860 px `68px` (Mobil-Leiste 52 px + 16 px) |
|
||||
| `.fin-body` | `container-type: inline-size` (einzige Stelle) |
|
||||
| `.fin-table-wrap` | `overflow-x: auto` |
|
||||
| `.fin-notice`, `.fin-page-notice` | `margin: 0 0 8px`, `margin-bottom: 16px` |
|
||||
| `.fin-load-error` | `display: grid; gap: 12px; justify-items: start; padding: 16px 0` |
|
||||
| `.fin-cols` | einspaltig, `gap: 0 56px` (den Zeilenabstand liefert `.page section + section`); ab 1100 px `minmax(0,1.8fr) minmax(0,1fr)`, `.fin-cols-half` 2 × `minmax(0,1fr)` und zusätzlich `.fin-stack .fin-cols > section + section { margin-top: 0 }`, damit beide Spalten oben bündig stehen (höhere Spezifität als die globale Regel, kein `!important`). |
|
||||
|
||||
Gelöscht: `.fin-spin`, `.fin-hint*`, `.fin-link*`, `.fin-stack .kpi-grid`. P5 legt den Reset für `button.link` an. Zeigt die Prüfung in `finanzen` oder `finanzen-tabellen` (Task 2) Browser-Rahmen an `button.link` oder `button.link-danger`, kommt `button.link-danger` in dieselbe Regel (`background: none; border: 0; padding: 0; font: inherit; cursor: pointer`).
|
||||
|
||||
- [ ] **Step 3: CSS-Blöcke 0, 1, 2**
|
||||
|
||||
- **Block 0:** Die Statuszeile hat keinen Kasten: `.fin-status` Flex, `space-between`, umbrechend, Abstand `8px 16px`, 13 px, `--text-2`. `.fin-status-items` Flex, Abstand `4px 24px`. `.fin-status-warn` `--orange-fg`, 600.
|
||||
- **Block 1:**
|
||||
- `.stats.fin-head` ist ein Raster `repeat(auto-fit, minmax(220px, 1fr))`, Abstand `24px 48px`, `margin: 0`. Die Handy-Regel von `.stats` (3 Spalten) würde die breite dritte Kennzahl abschneiden. Die Doppelklasse schlägt `.stats` durch Spezifität.
|
||||
- `.stats.fin-head .sub` 13 px `--text-2`. `.fin-breakdown-total` mit `border-top: 1px solid var(--line)`, 600.
|
||||
- `.fin-lows` Raster `auto auto minmax(0,1fr)`. `.fin-low .fin-amount` 18/26 px, 600.
|
||||
- **Block 2:** `.fin-todos` und `.fin-todo` mit Haarlinien `var(--line)`. `.fin-todo` Raster `80px minmax(0,1fr) auto`, Abstand `12px 16px`, `padding: 12px 0`, bei ≤ 860 px einspaltig. Die Streifenklassen `.fin-todo-hoch/-mittel` und `.fin-todos-more` entfallen.
|
||||
|
||||
- [ ] **Step 4: `Kpi` und `.kpi*` löschen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git rm -q src/components/ui/Kpi.jsx`. Dann die Zeile `export { default as Kpi }` aus `src/components/ui/index.js` und alle `.kpi`-Regeln samt `.fin-head .kpi-value` aus `global.css` entfernen (falls P1 sie nicht schon gelöscht hat).
|
||||
Expected: Nach Step 5 liefert `rg -n "Kpi|kpi" src` keinen Treffer.
|
||||
|
||||
- [ ] **Step 5: Komponenten umbauen**
|
||||
|
||||
- `FinancePage`:
|
||||
- Wurzel `.page`. Kopf: `PageHeader`, „Einstellungen“ als `Button as={Link}` ohne Icon. Ladezustand `loading`.
|
||||
- Ohne Daten: `msg err` plus `Button variant="primary"` „Erneut versuchen“, ohne `Card` und `style`.
|
||||
- Mit Daten: `.fin-stack` > `[Teilfehler: msg warn mit .link „Erneut versuchen“]`, `.fin-top` (`StatusLine`, `LiquidityHead`), `TodoList`, `.fin-cols` (`ForecastTable`, `TaxCard`), `.fin-cols.fin-cols-half` (`AboCard`), `InvoiceTable`, `TransactionTable`.
|
||||
- `assignNotice` als `msg warn fin-page-notice` mit `.link` „Schließen“. `AssignDialog` bleibt am Ende der `.page`.
|
||||
- `StatusLine`: Zeilen als `.fin-status-item` (Warnungen `.fin-status-warn`, ohne Icon). „Aktualisieren“/„Aktualisiere …“ ist ein Standardknopf. Hinweis über `noticeProps(hint.limited ? 'info' : 'warn')`.
|
||||
- `LiquidityHead`:
|
||||
- `<div className="stats fin-head">` mit drei `Stat`. Kontostand: `tone` = `isNegText(euroWhole(…)) ? 'neg' : ''`. Verfügbar: `tone` = `shortfall ? 'neg' : ''`, Flagge „Rücklagen nicht gedeckt“ als `.fin-flag`.
|
||||
- Tiefster Stand ohne `tone`, Beträge in `.fin-lows` bekommen `.fin-neg` bei `low.negative`.
|
||||
- „Aufschlüsselung“ ist `<button className="link">` mit `<span aria-hidden="true">▾/▴</span>`, die Zeilen von `breakdownRows` bleiben neutral.
|
||||
- `TodoList`: `Card` mit Badge „n offen“ als Aktion. Zeile `.fin-todo` ohne Zusatzklasse für die Dringlichkeit, Aktion als `<button className="link">` („Moment …“ während des Laufs, alle gesperrt), Fehler als `msg warn`. „Alle N anzeigen“ als `.link` in `<div className="table-foot">`.
|
||||
|
||||
- [ ] **Step 6: Hygiene, Bau, Bild**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|ui-btn|Kpi" src/pages/FinancePage.jsx src/components/finance/{StatusLine,LiquidityHead,TodoList}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t3" --only finanzen,finanzen-minus,finanzen-anker,finanzen-teilausfall,finanzen-fehler --gate p6`
|
||||
Expected: `rg` ohne Treffer, Gate Exit 0. In `finanzen-minus.png`: drei Kennzahlen nebeneinander, Kontostand und Verfügbar rot, beim „Tiefsten Stand“ nur der negative Betrag, Aufgaben als Haarlinienzeilen ohne Streifen.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/pages/FinancePage.jsx src/components/finance/StatusLine.jsx src/components/finance/LiquidityHead.jsx src/components/finance/TodoList.jsx src/components/ui/index.js
|
||||
git commit -m "Finanzen: Seitenkopf, Statuszeile, Kennzahlen und Aufgaben ohne Karten; Kpi entfernt" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
(`git rm` hat `Kpi.jsx` schon gestaged.)
|
||||
|
||||
### Task 4: Vorschau, Steuer, Abos
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/finance/ForecastTable.jsx`, `TaxCard.jsx`, `AboCard.jsx`, `src/styles/global.css` (Blöcke 3, 4, 5)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `.fin-body`, `.fin-table-wrap`, `.fin-neg`, `.fin-cols` (Task 3), `Stat`, `.table`, `.stats`.
|
||||
- Produces: Signaturen unverändert (`ForecastTable({ vorschau, kontostandCents, stripeDown, abosFehler })`, `TaxCard({ steuer })`, `AboCard({ abos })`).
|
||||
|
||||
- [ ] **Step 1: CSS Blöcke 3, 4, 5**
|
||||
|
||||
- **Vorschau:** `.fin-forecast-label` `overflow-wrap: anywhere`. Randzellen-Padding, `th:first-child` und Hover-Regeln entfallen. `@container (max-width: 560px)` bleibt mit denselben Flächen (`date kind amount / label / sicher erwartet`), Trenner `1px solid var(--line)`, `padding: 10px 0`.
|
||||
- **Steuer:** `.fin-facts` Raster, Abstand 16 px. `.fin-fact` mit Haarlinie unten `var(--line)`, der letzte ohne. `.fin-fact-big` 22/30, 600. `.fin-fact-unknown` `--orange-fg`.
|
||||
- **Abos:** `.fin-abos` und `.fin-abo` mit Haarlinien `var(--line)`. `.fin-abo` Raster `minmax(0,1fr) auto`, `padding: 12px 0`.
|
||||
- Gelöscht: `.fin-stats`, `.fin-stat-*`, `.fin-todo-card`, `.fin-forecast-card`, `.fin-tax-card`, `.fin-abo-card`.
|
||||
|
||||
- [ ] **Step 2: Komponenten umbauen**
|
||||
|
||||
- `ForecastTable`: `Card title` ohne `className` und `bodyStyle`. Darunter `.fin-body` > `.fin-table-wrap` > `<table className="table fin-forecast …">` (Rollen, `caption.fin-sr`, `data-label` unverändert). Betragszelle mit `fin-neg` bei `isNegText(row.amountText)`. Hinweise unter der Tabelle als `msg warn` (`role="status"`) ohne Icon, die Annahme als `msg muted`.
|
||||
- `TaxCard`: gleiche Struktur ohne Card-Klasse. `v.noteText` steht als `<p className="msg muted fin-tax-note">`, Wortlaut unverändert (rechtlich relevant).
|
||||
- `AboCard`: `<div className="stats fin-abo-stats">` (Haken für `finanzen-minus`, ohne eigene Regel) mit `Stat` „Abo-Einnahmen pro Monat“, „zahlende Kunden“ und, nur wenn gesetzt, „gefährdet“ mit `tone="neg"`. Liste wie bisher, Fehlerfall `msg warn` ohne Icon.
|
||||
|
||||
- [ ] **Step 3: Hygiene, Bau, Bild**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|bodyStyle|Kpi" src/components/finance/{ForecastTable,TaxCard,AboCard}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t4" --only finanzen,finanzen-minus,finanzen-teilausfall --gate p6`
|
||||
Expected: `rg` ohne Treffer, Exit 0. Bild: bei 390 px Vorschau-Zeilen dreizeilig (Datum/Art/Betrag, Posten, beide Salden), bei 1440 als Tabelle; Steuer als Liste mit Haarlinien.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/components/finance/ForecastTable.jsx src/components/finance/TaxCard.jsx src/components/finance/AboCard.jsx
|
||||
git commit -m "Finanzen: Vorschau, Steuer und Abos ohne Karten, Container-Query auf Wrapper" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 5: Rechnungen, Buchungen, Blätterleiste, Kundenreiter
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/finance/InvoiceTable.jsx`, `TransactionTable.jsx`, `Pager.jsx`, `AddressWarning.jsx`, `CustomerInvoices.jsx`, `src/styles/global.css` (Block 6 und 7 ohne Dialoge)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `.fin-body`, `.fin-table-wrap`, `.fin-notice`, `.fin-load-error`, `.table`, `.seg`, `.toolbar`, `.table-foot`, `.stats`, `Stat`, `noticeProps`, `isNegText`.
|
||||
- Produces: Signaturen unverändert (`InvoiceTable`, `TransactionTable`, `Pager({ page, pages, total, noun, onPage })`, `AddressWarning({ customer, onShowOverview, variant })`, `CustomerInvoices({ customer, onShowOverview })`) und `export const TRANSACTIONS_ANCHOR = 'finanzen-buchungen'`.
|
||||
|
||||
- [ ] **Step 1: CSS Block 6 und 7**
|
||||
|
||||
- `.stats.fin-ages`: Liste ohne Zeichen, Raster `repeat(4, minmax(0,1fr))`, Abstand `16px 32px`, `margin: 0 0 16px`; `.fin-ages .stat .v` 18/26 px; `@container (max-width: 860px)` 2 Spalten.
|
||||
- `.fin-block .seg { flex-wrap: wrap; max-width: 100% }`, bei ≤ 860 px Knopf-Padding `0 9px`. `.fin-pager` (mit `.table-foot`): `justify-content: center; gap: 16px`, `.fin-pager-info` tabular.
|
||||
- Tabellen: Abstands- und Randzellenregeln entfallen (`.table` aus P1 gilt). Es bleiben `.fin-inv-customer` und `.fin-tx-counterparty` mit `overflow-wrap: anywhere`, `.fin-inv-number`, `.fin-inv-created/-due` und `.fin-tx-date` nowrap, `.fin-inv-statusbox` Raster 3 px, `.fin-actions` Flex `gap: 4px 14px`, `.fin-tx-remittance` mit 2 Zeilen Klemme, `.fin-tx-assign .badge { white-space: normal; height: auto; padding-block: 2px }` (zwei Klassen schlagen `.badge`), `.fin-tx-action` nowrap rechts.
|
||||
- `@container (max-width: 860px)` (Rechnungen) und `(max-width: 640px)` (Buchungen) bleiben mit den Flächen `number status / customer / created due / total remaining / actions` bzw. `date amount / what / assign action`, Trenner `var(--line)`, `padding: 10px 0`.
|
||||
- `.fin-address-warn` Flex umbrechend `4px 12px`; `-page` `margin: 0 0 16px`, `-dialog` `margin: 0`.
|
||||
|
||||
- [ ] **Step 2: `InvoiceTable` und `Pager`**
|
||||
|
||||
- Aufbau: `<section id={…ANCHOR} className="fin-block">` > `Card title="Rechnungen"` mit schwarzem „Rechnung schreiben“ als Aktion > `.fin-body` (Altersstreifen, `.toolbar` mit `.seg`, Meldung, Inhalt, `Pager`). `InvoiceCreateModal` bleibt Geschwister der `Card`.
|
||||
- Filter: `.seg` mit `aria-pressed` und Klasse `on`. Meldung über `noticeProps(message.tone, 'fin-notice')`. Altersstreifen: `Stat label={cell.label} value={cell.sumText} sub={…Rechnung(en)}` je `<li>`.
|
||||
- Zeilenaktionen: „Zahlungsseite“ und „PDF“ als `<a className="link" target="_blank" rel="noopener noreferrer">` mit unverändertem `aria-label`, „Senden“ und „Erinnerung senden“ als `.link`-Knopf, „Stornieren“ und „Löschen“ als `Button variant="danger"`. Sperren, Laufbeschriftungen („Sende …“, „Storniere …“, „Lösche …“) und `title` am gesperrten „Senden“ bleiben.
|
||||
- Tabelle `<table className="table fin-invoices" role="table">`, Rollen, `caption.fin-sr`, `data-label` unverändert. `Pager`: `<nav className="table-foot fin-pager">` mit „Zurück“/„Weiter“ als `Button variant="quiet"`.
|
||||
|
||||
- [ ] **Step 3: `TransactionTable`, `AddressWarning`, `CustomerInvoices`**
|
||||
|
||||
- `TransactionTable`: `Card title="Buchungen"` in `<section id=TRANSACTIONS_ANCHOR className="fin-block">`, Inhalt in `.fin-body`. Laden `loading`, Fehler `fin-load-error` mit `msg warn` und Standardknopf, Stripe-Hinweis `msg warn` ohne Icon. Betrag `fin-amount fin-tx-amount` plus `fin-neg` bei `isNegText`, `fin-tx-out` bleibt. „Zuordnen“/„Ändern“ als `.link`-Knopf mit `aria-label`.
|
||||
- `AddressWarning`: `<div className="msg warn fin-address-warn fin-address-warn-{variant}" role="alert">` mit `<button type="button" className="link">` bzw. `Link className="link"`. Der Knopf trägt immer `type="button"`, weil die Variante `dialog` in einem `Dialog as="form"` steht.
|
||||
- `CustomerInvoices`: Sperrfall als `EmptyState` ohne `Card`, „Daten erfassen“ bleibt schwarz. Sonst `AddressWarning` und `InvoiceTable`.
|
||||
|
||||
- [ ] **Step 4: Hygiene, Bau, Bild**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|bodyStyle|FaSpinner" src/components/finance/{InvoiceTable,TransactionTable,Pager,AddressWarning,CustomerInvoices}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t5" --only finanzen,finanzen-tabellen,finanzen-pager,finanzen-anker,finanzen-teilausfall,kunde-invoices,kunde-invoices-adresse --gate p7`
|
||||
Expected: `rg` ohne Treffer, Exit 0 (kein `overflow-x` bei 390 px trotz langer Namen), alle Schritte laufen durch. Bild: Altersstreifen 4 Spalten (1440) bzw. 2 (390), Filter als graue Schiene, Tabellen mit Haarlinien und Textaktionen.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/components/finance/InvoiceTable.jsx src/components/finance/TransactionTable.jsx src/components/finance/Pager.jsx src/components/finance/AddressWarning.jsx src/components/finance/CustomerInvoices.jsx
|
||||
git commit -m "Finanzen: Rechnungen und Buchungen als Tabellen ohne Karten, Textaktionen, Blaetterleiste" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Einstellungen, Bankzugang, Fixkosten, Bankrückkehr, `FormField`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/FinanceSettingsPage.jsx`, `src/pages/BankCallbackPage.jsx`, `src/components/finance/SettingsCard.jsx`, `BankAccessCard.jsx`, `FixedCostsCard.jsx`, `FormField.jsx`, `src/styles/global.css` (Region „Finanzen: Einstellungen“)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `.field`, `.hint`, `.field-row`, `.input/.select/.textarea` mit Fehlerring bei `[aria-invalid="true"]` (P1), `.kv`, `.msg`, `noticeProps`.
|
||||
- Produces: `FormField({ id, label, hint, error, children })` unverändert. Das Kind bekommt weiter `id`, `aria-invalid`, `aria-describedby`.
|
||||
|
||||
- [ ] **Step 1: CSS Einstellungen**
|
||||
|
||||
- `.fin-fieldset` Raster `gap: 16px`, ohne Rand und Innenabstand. `.fin-form-field { min-width: 0 }`. `.fin-input-amount` rechts. `.fin-counter` 12 px `--text-2` rechts, `margin-top: -8px`, `.fin-counter-over` `--danger` (Formularfehler), 600. `.fin-form-actions` Flex, `gap: 12px`, `margin-top: 16px`.
|
||||
- `.fin-fixed-form` ohne Rahmen, nur `padding-bottom: 24px; margin-bottom: 8px; border-bottom: 1px solid var(--line)`. `.fin-form-title` 14/20, 600. `.fin-result` 16 px, 600.
|
||||
- `.fin-fixed-list` und `.fin-fixed` mit Haarlinien `var(--line)`. `.fin-fixed` Raster `minmax(0,1fr) auto`, `padding: 12px 0`. `.fin-fixed-off` Name und Betrag `--text-2`.
|
||||
- Gelöscht: `.fin-form-grid`, `.fin-bank-facts*`, `.fin-field-error`, `.fin-form-hint`, `.fin-form-notice`, `.fin-form-field .form-control[aria-invalid]`.
|
||||
|
||||
- [ ] **Step 2: Komponenten umbauen**
|
||||
|
||||
- `FormField`: `.field fin-form-field`, Hinweis `<div className="hint">`, Fehler `<div className="msg err" role="alert">`. Den Fehlerring liefert allein `aria-invalid`.
|
||||
- `SettingsCard`: Feldpaare als `.field-row`, Felder `input fin-input-amount`, Textarea `textarea`, Ladefehler `fin-load-error` mit `msg warn`. „Speichern“/„Speichere …“ schwarz ohne Icon. Meldung über `noticeProps(notice.tone)`.
|
||||
- `BankAccessCard`: `Card` mit Badge als Aktion. Fakten als flaches `<dl className="kv">` (`<dt>`/`<dd className="num">` ohne `<div>`-Gruppen). Schwarz ist „Jetzt abrufen“ (bei `canRefresh`), sonst „Bank verbinden“, der andere Knopf ist Standard. „Rufe ab …“ und „Leite weiter …“ bleiben.
|
||||
- `FixedCostsCard`: „Neue Position“ im Kopf als Standardknopf ohne Plus. Formular mit `<h3 id="fin-fix-form-title" className="fin-form-title">`, `.field-row`, „Speichern“ schwarz, „Abbrechen“ Standard. Zeilenaktionen: „Bearbeiten“ und `row.toggleLabel` als `.link`, „Löschen“ als `Button variant="danger"`. `aria-label`, `window.confirm` und Sperren bleiben.
|
||||
|
||||
- [ ] **Step 3: Seiten**
|
||||
|
||||
- `FinanceSettingsPage`: `PageHeader` mit „← Zurück zu den Finanzen“ als `Button as={Link}` (Standard, Pfeil ist Zeichen). Inhalt `.fin-stack` mit den drei Abschnitten.
|
||||
- `BankCallbackPage`: ohne `Card` und `style`. Ergebnis `<p className="msg ok fin-result">` bzw. `msg err fin-result` mit unveränderter `role`. Laden `loading` („Bankzugang wird eingerichtet …“). „Zu den Finanzen“ bleibt schwarz.
|
||||
|
||||
- [ ] **Step 4: Hygiene, Bau, Bild**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|size=\"sm\"|form-control|form-group|FaSpinner" src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance/{SettingsCard,BankAccessCard,FixedCostsCard,FormField}.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t6" --only finanzen-einstellungen,finanzen-einstellungen-fehler,finanzen-bank-callback,finanzen-bank-callback-ok --gate p7`
|
||||
Expected: `rg` ohne Treffer, Exit 0, alle Schritte laufen durch. Bild `finanzen-einstellungen-fehler`: roter Ring und Fehlertext am Feld, keine Kästen.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx src/components/finance/SettingsCard.jsx src/components/finance/BankAccessCard.jsx src/components/finance/FixedCostsCard.jsx src/components/finance/FormField.jsx
|
||||
git commit -m "Finanzen: Einstellungen, Bankzugang, Fixkosten und Bankrueckkehr ohne Karten" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: `AssignDialog` auf `Dialog`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/finance/AssignDialog.jsx`, `src/styles/global.css` (Dialog-Regionen für die Zuordnung)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Dialog({ title, onClose, busy, wide, footer, children })` (P1), `Field`, `.check`, `.kv`, `.msg`, `useScrollIntoView`.
|
||||
- Produces: `AssignDialog({ transaction, invoices, invoicesLoading, invoicesError, invoicesLoaded, onClose, onDone })` unverändert. `onClose({ refresh })` und `onDone(buchung)` verhalten sich wie bisher.
|
||||
|
||||
- [ ] **Step 1: CSS Zuordnen**
|
||||
|
||||
- `.fin-foot`: Flex, `flex: 1`, umbrechend, `justify-content: flex-end`, Abstand 8 px; bei ≤ 860 px `.fin-foot .btn { flex: 1 1 auto }`.
|
||||
- `.fin-choices`: Raster ohne Rand. `.fin-choice` (mit `.check`): volle Breite, `padding: 8px 0`, Haarlinie unten. Mit gewähltem Radio ist `.fin-choice-label` 600 (`:has(input:checked)`). `.fin-choice-off` `opacity: .55; cursor: not-allowed`. `.fin-choice-detail` 13 px `--text-2`, mit `.warn` `--orange-fg`.
|
||||
- `.fin-confirm` 600 in `--orange-fg`, kein Kasten. `.fin-assigned { margin: 0 }`. `.kv .fin-tx-facts-amount { text-align: left; font-weight: 600 }`.
|
||||
- Gelöscht: `.fin-tx-facts*`, `.fin-confirm:focus`, Kastenregeln von `.fin-choice`.
|
||||
|
||||
- [ ] **Step 2: Komponente umbauen**
|
||||
|
||||
- Eigener Esc-Listener, `useDialogFocus`, `dialogRef`, Overlay-Markup und „ד-Knopf entfallen. Es bleiben `lockRef`, `aliveRef`, `dirtyRef`, `useScrollIntoView(errorRef, error)` und `confirmRef`.
|
||||
- Gerüst `<Dialog title="Buchung zuordnen" onClose={close} busy={disabled} footer={…}>`. `close()` ist parameterlos (das „ד ruft es mit dem Klick-Event), prüft `lockRef` und ruft `onClose({ refresh: dirtyRef.current })`.
|
||||
- Body: Fakten als flaches `<dl className="kv">`, Fehler `msg err` mit `ref={errorRef}`, Hinweise `msg muted`/`msg warn` mit unveränderten Rollen. Vorschläge und Kategorien über `radio(o)` als `<label className="check fin-choice…">`. „Andere offene Rechnung“, „Fixkosten“ und „Notiz (optional)“ sind `Field` mit `select`/`textarea`. Beim Bestätigen steht der Text im `.fin-confirm` (`role="group"`, `tabIndex={-1}`, Fokus per `confirmRef` wie heute).
|
||||
- Fuß je Zustand: zugeordnet „Schließen“ und, bei `canUnassign`, „Zuordnung lösen“ (beide Standard). Bestätigend „Zurück“ (Standard) und „Ja, als bezahlt markieren“ (schwarz, „Markiere …“). Sonst „Abbrechen“ (Standard) und „Zuordnen“ (schwarz, „Ordne zu …“, gesperrt ohne Auswahl).
|
||||
- Der Zweischritt bleibt: Bei einer Rechnung geht vor der Bestätigung keine Anfrage raus.
|
||||
|
||||
- [ ] **Step 3: Hygiene, Bau, Prüfung**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|useDialogFocus|keydown|overlay|fin-overlay|fin-close|form-control|form-label" src/components/finance/AssignDialog.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t7" --only dlg-zuordnen-zahlung,dlg-zuordnen-bestaetigung,dlg-zuordnen-busy --gate p7`
|
||||
Expected: `rg` ohne Treffer, Exit 0, alle drei Schritte laufen durch. Bild: weißer Dialog, Vorschläge als Radiozeilen mit Haarlinien, bei 390 px Bottom-Sheet mit umbrechendem Fuß.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/components/finance/AssignDialog.jsx
|
||||
git commit -m "Finanzen: Buchung zuordnen auf Dialog-Baustein (Esc, Sperre, Vorschlaege als Zeilen)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: `InvoiceCreateModal` auf `Dialog`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/finance/InvoiceCreateModal.jsx`, `src/styles/global.css` (Rest der Dialog-Region)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Dialog`, `Field`, `AutocompleteInput` (P1, rendert `.input`), `AddressWarning`, `.fin-foot` (Task 7).
|
||||
- Produces: `InvoiceCreateModal({ open, onClose, onCreated, fixedCustomer, onShowOverview })` unverändert, samt Vertrag `onCreated(rechnung, info)`.
|
||||
|
||||
- [ ] **Step 1: CSS Rechnung schreiben**
|
||||
|
||||
- `.fin-items` ohne Rand. `.fin-item` Raster `minmax(0,1fr) 80px 120px 110px auto`, Abstand `8px 12px`, `align-items: end`. `.fin-item .input { width: 100% }` (zwei Klassen schlagen `.input`), `.fin-item .fin-amount` rechts. `.fin-item-label` 12 px `--text-2`, ab der zweiten Zeile visuell versteckt. `.fin-items-foot` Flex `space-between`, `margin-top: 8px`. `.fin-total` Flex, `gap: 16px`, 600.
|
||||
- `.fin-dialog-row` Raster `180px minmax(0,1fr)`, Abstand 12 px.
|
||||
- Bei ≤ 860 px: `.fin-item` Raster `minmax(0,.7fr) minmax(0,1fr) minmax(0,1fr) auto` mit Flächen `desc desc desc desc / qty price sum remove`, Labels sichtbar. `.fin-dialog-row` einspaltig.
|
||||
- `.autocomplete .input { width: 100% }` legt P6 an (`rg -n "\.autocomplete \.input" src/styles/global.css`). Fehlt sie, kommt sie hierher (Prüfung in `dlg-rechnung-kunde`).
|
||||
- Alte Hüllenregeln löschen: `.fin-close*`, `.fin-overlay`, `.fin-dialog` samt `:focus`, `.fin-dialog-error`, `.fin-dialog-warn`, `.fin-dialog-sub`, `.fin-dialog-actions`, `.fin-icon-btn*`.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fin-(overlay|close|icon-btn)" src; rg -n "fin-dialog" src | rg -v "fin-dialog-row"`
|
||||
Expected: Vor Step 2 trifft es noch `InvoiceCreateModal.jsx`, nach Step 2 höchstens den KOMPAT-Block.
|
||||
|
||||
- [ ] **Step 2: Komponente umbauen**
|
||||
|
||||
- Eigener Esc-Listener, `useDialogFocus`, `dialogRef`, Overlay und „ד-Knopf entfallen. Es bleiben `lockRef`, `aliveRef`, `errorRef` mit `useScrollIntoView`.
|
||||
- Gerüst `<Dialog as="form" wide title="Rechnung schreiben" onClose={close} busy={disabled} onSubmit={(e) => e.preventDefault()} footer={…}>`. Das Formular hat keinen Submit-Knopf. Jeder Knopf darin trägt `type="button"` (`Button` setzt keinen Standard-Typ): „Abbrechen“, „Als Entwurf speichern“, „Senden“, „+ Position“, „Entfernen“ und der Knopf in `AddressWarning`. Deshalb sendet Enter nie. `close()` prüft `lockRef` und ruft `onClose()`.
|
||||
- Body: zuerst Fehler `msg err` mit `ref={errorRef}`. Kunde als `Field` mit `htmlFor="fin-invoice-customer"` (fester Kunde oder `AutocompleteInput`), Hinweise `hint`/`msg warn`, `AddressWarning variant="dialog"` mit dem bisherigen `onShowOverview`-Handler.
|
||||
- Positionen als `<fieldset className="fin-items">` mit `.fin-item`-Zeilen (`input`, `fin-amount`, `aria-label`s unverändert). „Entfernen“ ersetzt den Papierkorb als `<button type="button" className="link">` mit `aria-label="Position n entfernen"`, gesperrt bei nur einer Position. „+ Position“ ist ein Standardknopf. Zahlungsziel und Notiz als `Field` in `.fin-dialog-row`.
|
||||
- Fuß `<div className="fin-foot">`: „Abbrechen“ und „Als Entwurf speichern“ (Standard), „Senden“ (schwarz, `disabled` und `title` wie heute). „Speichere …“ und „Sende …“ bleiben.
|
||||
- Esc: Ein offenes Vorschlagslisten-Popover ruft `preventDefault`, dann schließt `Dialog` nicht. Ohne Liste schließt Esc über `close`.
|
||||
|
||||
- [ ] **Step 3: Hygiene, Bau, Prüfung**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|useDialogFocus|keydown|overlay|form-control|form-group|form-label|FaTrash|FaPlus" src/components/finance/InvoiceCreateModal.jsx; "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7-t8" --only dlg-rechnung,dlg-rechnung-kunde,dlg-rechnung-enter,kunde-invoices-adresse --gate p7`
|
||||
Expected: `rg` ohne Treffer, Exit 0, alle Schritte laufen durch (Overlay in Fenstergröße, Esc-Reihenfolge, Enter ohne Request, Feldbreite). Bild bei 390 px: Positionszeilen zweizeilig, Fuß mit umbrechenden Knöpfen, Vorschlagsliste sichtbar.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/components/finance/InvoiceCreateModal.jsx
|
||||
git commit -m "Finanzen: Rechnung schreiben auf Dialog-Baustein (breit, Enter sendet nie, Esc schliesst Liste zuerst)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 9: Gate P7, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0, `# pass` = N + 2 und `# fail 0`. Die Größen von JS und CSS aus dieser Ausgabe gehen in Step 3.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf, Hygiene und tote Klassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p7 && "$W/shots.sh" p7 "$W/shots/p7" --gate p7`
|
||||
Expected: Exit 0. Alle Prüfungen aus P1 sind für alle Ansichten leer, `icon-svg` für alle `FIN`-Ansichten und die der früheren Pakete.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh || exit 1
|
||||
F="src/components/finance src/pages/FinancePage.jsx src/pages/FinanceSettingsPage.jsx src/pages/BankCallbackPage.jsx"
|
||||
finblock() { sed -n '/^\/\* ===== FINANZEN (P7)/,/^\/\* ===== ENDE FINANZEN/p' src/styles/global.css; }
|
||||
rg -n "react-icons|@tabler|style=\{|size=\"sm\"|ui-btn|form-control|form-group|form-label|overlay-content|overlay-close|spinner|Kpi|kpi|#[0-9a-fA-F]{3,8}\b|rgba?\(" $F
|
||||
finblock | rg -n "#[0-9a-fA-F]{3,8}\b|rgba?\(|!important|var\(--(surface|border|text-muted|text-faint|r-|shadow-[123]|warn|info|ok)\b"
|
||||
finblock | rg -o "\.fin-[a-z0-9-]+" | sort -u | sed 's/^\.//' | while read c; do rg -q "(^|[^A-Za-z0-9_-])${c}([^A-Za-z0-9_-]|$)" src --glob '!*.css' || echo "TOT $c"; done
|
||||
finblock | wc -l; finblock | rg -c "container-type"
|
||||
git diff --name-only design-p6..HEAD
|
||||
```
|
||||
Expected:
|
||||
- Die ersten beiden Befehle geben nichts aus.
|
||||
- Der dritte nennt nur `fin-address-warn-dialog` und `fin-address-warn-page` (per Template-Präfix gebaut, erlaubt).
|
||||
- Der Finanzblock hat 250 bis 360 Zeilen, `container-type` steht genau einmal da (`.fin-body`).
|
||||
- Der letzte Befehl (`git diff --name-only design-p6..HEAD`) nennt nur die Dateien aus „Global Constraints / Dateien“ und `tests/unit/financeUi.test.mjs`.
|
||||
|
||||
- [ ] **Step 3: Bundle-Größe**
|
||||
|
||||
Die JS- und CSS-Größe aus der `check.sh`-Ausgabe von Step 1 wird mit Task 1 Step 1 verglichen. Keiner der beiden Werte ist größer.
|
||||
|
||||
- [ ] **Step 4: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p6..HEAD` gegen die Spec-Abschnitte „Gestaltungsregeln“, „Bausteine“ und Paket P7. Prüfaufträge:
|
||||
- Verhalten gleich geblieben: Sperren, `window.confirm`, Zweischritt in `AssignDialog`, Rollen und `data-label` der Tabellen.
|
||||
- `busy` und `close()` sind in beiden Dialogen richtig angeschlossen, kein Dialog sitzt in `.fin-body`.
|
||||
- Jede Klasse `fin-*` ist definiert oder bewusst entfallen, Rot nur bei Minus, Texte nur wie in „Global Constraints“ geändert.
|
||||
Wichtige Befunde werden behoben, Kleinigkeiten notiert. Nach Korrekturen kommen Step 1 und Step 2 noch einmal, damit die Bilder den Stand zeigen, der getaggt wird.
|
||||
|
||||
- [ ] **Step 5: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p7 && git log --oneline origin/test..design-p7 && git rev-parse design-p7`
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- die Screenshots aus `shots/p7/1440x900` und `shots/p7/390x844` (Auswahl: `finanzen`, `finanzen-minus`, `finanzen-teilausfall`, `dlg-rechnung-kunde`, `dlg-zuordnen-bestaetigung`, `finanzen-einstellungen-fehler`, `kunde-invoices-adresse`)
|
||||
- den Hinweis, dass negative Beträge jetzt rot sind und beide Dialoge neu auch mit Esc schließen
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p7^{commit}:refs/heads/test'`
|
||||
- die Rücknahme (nur solange P7 das jüngste Paket ist): `git revert --no-commit design-p6..design-p7`, Commit und Push durch den Eigentümer
|
||||
- den Hinweis für P10: Die KOMPAT-Zeilen zu `.fin-link` und `.fin-dialog` (760 px) sind tot und fallen mit dem Block weg.
|
||||
|
||||
- [ ] **Step 6: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers: Finanzen mit Statuszeile und Rechnungsliste, Scroll von „Zu den Rechnungen“. Keinen Dialog öffnen, der etwas sendet.
|
||||
@@ -0,0 +1,664 @@
|
||||
# WOMS „Weißes Design“ P8: Rechtliches und Admin – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P8 sind „Rechtliches“ und „Admin“ kastenlos und ohne Icons. Rechtstexte stehen als Abschnitte mit Haarlinien-Zeilen, die Anbieter-Verträge als Tabelle. Vertrags-, Versions- und Mitarbeiter-Formular sind Dialoge, die nur bei Erfolg schließen und Fehler im Dialog zeigen. Jede heutige Funktion bleibt erreichbar.
|
||||
|
||||
**Architecture:**
|
||||
- Zwei neue reine Lib-Dateien mit Tests: `src/lib/legalView.js` (Anzeige-Texte der Vertragsliste, Badge-Anteil der Frist) und `src/lib/employeeForm.js` (Prüfung des Mitarbeiter-Formulars).
|
||||
- Bausteine aus P1: `Dialog`, `Field`, `Card`, `Badge`, `Tabs`, `PageHeader`, `EmptyState`, `.table`, `.msg`, `.loading`, `.link`.
|
||||
- CSS: Der Rechtliches-Block in `global.css` und `AdminPage.css` (416 → rund 45 Zeilen) werden neu geschrieben. `.credential*`, `.sr-only` und `.spin` ziehen nach `global.css`.
|
||||
- Hooks (`useLegal`, `useEmployees`) und `lib/legal.js` bleiben unverändert.
|
||||
|
||||
**Tech Stack:** React 18, react-router-dom 6, reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Bausteine“ (Abschnitt, Knöpfe, Links, Badge, Tabelle, Formular, Dialog, Meldungen, Laden), „Texte“ und Paket P8. Anhang `2026-10-04-weisses-design-umbaukarte.md`: Abschnitt 3 (Dialog-Verbraucher), 4 (Icon-Regel), 5 (P8), 7.4, 7.8. Globale Regeln und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P8 kommt hinzu:
|
||||
|
||||
- Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build werden nur über `"$W/check.sh"` ausgewertet, Browser-Skripte laufen nur über `"$W/shots.sh"`.
|
||||
- Ausgangspunkt ist das Tag `design-p7`, alle Diff-Prüfungen vergleichen gegen `design-p7`.
|
||||
- Unverändert bleiben `src/hooks/useLegal.js`, `useEmployees.js`, `useDialogFocus.js`, `useScrollIntoView.js` und `src/lib/legal.js`. Beide `window.confirm`-Rückfragen („Vertrag „…“ wirklich löschen?“, „… und dessen Login wirklich löschen?“) bleiben wörtlich.
|
||||
- Sichtbare Textänderungen sind nur: „Bearbeiten“ und „Löschen“ in der Admin-Tabelle (waren Icons), „Speichert …“ im Speichern-Knopf des Admin-Formulars (war Spinner) und der Leer-Hinweis der Admin-Tabelle (Task 9).
|
||||
- Dialoge schließen nur über „ד, Esc und bei Erfolg. Bei Fehlern bleiben Dialog und Eingaben offen. Solange gespeichert wird, sind „ד, Esc und „Abbrechen“ gesperrt.
|
||||
- Jeder Knopf in einem `<Dialog as="form">` außer dem Absenden-Knopf trägt `type="button"` (`Button` setzt keinen eigenen Standard-Typ).
|
||||
- Abstände zwischen aufeinanderfolgenden Abschnitten kommen von `.page section + section { margin-top: 36px }` aus P1. P8 baut dafür keine eigenen Regeln (kein `.legal-doc + .legal-doc`, kein `.admin-page section + section`).
|
||||
- CSS-Reihenfolge: `main.jsx` importiert `App` vor `global.css`, also lädt `AdminPage.css` vor `global.css`, und bei gleicher Spezifität gewinnt `global.css`. Regeln in `AdminPage.css`, die eine Global-Klasse überstimmen, haben höhere Spezifität (0,2,0, z. B. `.page.admin-page …`, `.check.admin-check`) und nie `!important`. Deshalb wirkt die heutige Breite 1480 px nie.
|
||||
- Die CSS-Tabellen dieses Plans nennen nur die Abweichungen vom Prototyp und von den Tokens. Was Basis (`*`-Reset, `button`-Reset), `.table`, `.kv`, `.field-row` und `.check` schon liefern, wird nicht wiederholt.
|
||||
- In den umgebauten Dateien: keine Icon-Importe, keine neuen `style={{…}}`, keine Farbliterale.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Aus der Übersicht gehören drei Punkte zu P8:
|
||||
- **Punkt 2, lange Inhalte am Handy:** Testdaten `lang-recht-admin` (Anbietername 60 Zeichen, Zweck 90, Notiz 300, Dateiname 70, E-Mail 70). `overflow-x` ist seit dem Gate `p1` für alle Ansichten scharf, also auch im Gate `p8`.
|
||||
- **Punkt 3, Teilausfall:** Ansicht `admin-ausfall` (Szenario `fehler`). Die Mitarbeiterliste antwortet 500, die Seite zeigt `.msg.err`, „Mitarbeiter anlegen“ öffnet weiter den Dialog.
|
||||
- **Punkt 4, nur Tastatur:** Esc schließt Vertrags- und Mitarbeiter-Dialog. Bei laufendem Speichern ist „ד gesperrt und Esc wirkungslos (`dlg-vertrag-fehler`, `admin-fehler`). Aufklappzeilen sind echte `<button>` mit `aria-expanded` und eigenem `onClick` (Enter und Klick auf den Button klappen auf).
|
||||
|
||||
Zusätzlich gilt: Das Seiten-Banner der Admin-Seite bleibt für Erfolg und Löschen, Validierungs- und Speicherfehler des Formulars stehen im Dialog. Checkliste der Bedienelemente, nichts darf fehlen:
|
||||
- Rechtliches: 2 Reiter, „Neue Version“, Verlauf auf-/zuklappen, „Live-Seite öffnen“, „Als gültig markieren“, Dateilinks mit „(herunterladen)“, „Neuer Vertrag“, Zeile aufklappen, „Bearbeiten“, „Als gekündigt markieren“ samt „Endet am“, „Speichern“, „Abbrechen“, „Löschen“ (mit Rückfrage), alle Felder und der Upload beider Dialoge.
|
||||
- Admin: Reiter, „Mitarbeiter anlegen“, „Bearbeiten“, „Löschen“ (nicht in der eigenen Zeile, mit Rückfrage), Kopieren von E-Mail und User-ID, Zugangsdaten-Abschnitt mit „Anzeigen“, „Kopieren“, „ד, Admin-Checkbox (für das eigene Konto gesperrt), Kürzel in Großbuchstaben.
|
||||
|
||||
---
|
||||
|
||||
|
||||
> **Nachtrag aus P0 (Gate):** Gate `p1` und alle folgenden Gates schließen `admin` und `admin-neu` von `overflow-x` aus (`['*', '!admin', '!admin-neu']`). P8 macht die Admin-Seite bei 390 px ohne waagerechtes Scrollen und entfernt die beiden Ausschlüsse aus allen Gates ab `p8` (ein Schritt im Abschluss-Task, geprüft mit `"$W/shots.sh" … --only admin,admin-neu --gate p8`).
|
||||
|
||||
### Task 1: Paketstart und Ausgangswerte
|
||||
|
||||
**Files:** keine Änderung.
|
||||
|
||||
**Interfaces:** keine.
|
||||
|
||||
- [ ] **Step 1: Paketstart**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p7 HEAD && git rebase origin/test && "$W/check.sh" && git status --short`
|
||||
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0` und eine leere Statusliste. Endet `merge-base` mit Exit 1, liegt `design-p7` nicht im Branch: stoppen und melden. Die Testzahl heißt im Folgenden N0, die Bundle-Größen aus der Ausgabe von `check.sh` werden notiert.
|
||||
|
||||
- [ ] **Step 2: Abgleich mit TP2**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/LegalPage.jsx src/components/legal src/lib/legal.js src/hooks/useLegal.js src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx src/components/CopyableCredential.jsx src/styles/global.css`
|
||||
Expected: keine Ausgabe. Stand 2026-10-05 berührt `225e2f1` die Rechtliches-Dateien. Gibt es Treffer, stoppt der Agent vor Task 2, nennt dem Eigentümer die Commits und fragt nach. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
|
||||
|
||||
- [ ] **Step 3: Voraussetzungen aus P1 bis P7 prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/components/ui/Dialog.jsx src/components/ui/Field.jsx src/components/ui/tones.js src/lib/rowClick.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -n "button\.link" src/styles/global.css && rg -n -m 6 "\.field-row|\.dialog-foot|\.msg\.err|\.loading|\.kv\b" src/styles/global.css`
|
||||
Expected: Die vier Dateien sind da. `src/lib/rowClick.js` (P2 Task 2) enthält `isRowActivation`. Fehlt die Datei, stoppt der Agent und meldet es, P8 baut keinen eigenen Zeilenklick-Helfer. `button.link` hat ein Zurücksetzen mit `background: none` und `border: 0` (P2 oder P5), und die Klassen sind definiert.
|
||||
|
||||
Entscheidung: Fehlt das Zurücksetzen, ergänzt P8 es im Commit von Task 5: `button.link { background: none; border: 0; padding: 0; font: inherit; text-align: left; cursor: pointer }`.
|
||||
|
||||
### Task 2: `legalView` (Lib, TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/legalView.js`
|
||||
- Test: `tests/unit/legalView.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `formatDate` und `deadlineTone` aus `src/lib/legal.js`, unverändert.
|
||||
- Produces, alles in `src/lib/legalView.js`:
|
||||
- `costLabel(contract)`, `termLabel(contract)`, `noticeLabel(contract)`, `deadlineLabel(result)` → String. Es sind die bisherigen lokalen Funktionen aus `ProviderContractsTab.jsx` Z. 8–36.
|
||||
- `deadlineView(result, today) → { text, lead, rest, badge, muted }` mit `badge: 'danger' | 'warn' | null`.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
```js
|
||||
const { costLabel, termLabel, noticeLabel, deadlineView } = await loadModule('src/lib/legalView.js')
|
||||
const NB = '\u00a0'
|
||||
const today = '2026-09-30'
|
||||
const active = (noticeBy, periodEnd = '2027-03-12') => ({ state: 'active', noticeBy, periodEnd })
|
||||
const v = (r) => deadlineView(r, today)
|
||||
|
||||
test('Vertragsliste: Kosten, Laufzeit, Frist wie bisher', () => {
|
||||
assert.equal(costLabel({ cost: null }), '–')
|
||||
assert.equal(costLabel({ cost: 12.5, costInterval: 'month' }), `12,50${NB}€/Monat`)
|
||||
assert.equal(costLabel({ cost: 120, costInterval: 'year' }), `120,00${NB}€/Jahr`)
|
||||
assert.equal(termLabel({}), '–')
|
||||
assert.equal(termLabel({ termMonths: 0 }), 'jederzeit kündbar')
|
||||
assert.equal(termLabel({ termMonths: 12, renewalMonths: 12 }), '12 Monate, dann je 12 Monate')
|
||||
assert.equal(termLabel({ termMonths: 24, renewalMonths: 0 }), '24 Monate, endet danach')
|
||||
assert.equal(noticeLabel({}), '–')
|
||||
assert.equal(noticeLabel({ noticeValue: 1, noticeUnit: 'months' }), '1 Monate')
|
||||
assert.equal(noticeLabel({ noticeValue: 14, noticeUnit: 'days' }), '14 Tage')
|
||||
})
|
||||
|
||||
test('deadlineView: Badge nur bei rot und orange, Text wörtlich wie bisher', () => {
|
||||
assert.deepEqual(v(active('2026-10-15')), {
|
||||
text: 'kündbar bis 15.10.2026 (Periode endet 12.03.2027)',
|
||||
lead: 'kündbar bis 15.10.2026', rest: '(Periode endet 12.03.2027)', badge: 'danger', muted: false,
|
||||
})
|
||||
assert.equal(v(active('2026-10-30')).badge, 'danger') // 30 Tage
|
||||
assert.equal(v(active('2026-10-31')).badge, 'warn') // 31 Tage
|
||||
assert.equal(v(active('2026-12-29')).badge, 'warn') // 90 Tage
|
||||
const normal = v(active('2026-12-30')) // 91 Tage
|
||||
assert.deepEqual([normal.badge, normal.lead === normal.text, normal.rest], [null, true, ''])
|
||||
})
|
||||
|
||||
test('deadlineView: Text und Dämpfung der übrigen Zustände', () => {
|
||||
assert.deepEqual([v({ state: 'anytime' }).text, v({ state: 'anytime' }).muted], ['jederzeit kündbar', false])
|
||||
assert.equal(v({ state: 'ends', periodEnd: '2027-01-31' }).text, 'endet am 31.01.2027')
|
||||
assert.deepEqual([v({ state: 'ended', periodEnd: '2026-01-31' }).text, v({ state: 'ended', periodEnd: '2026-01-31' }).muted], ['beendet seit 31.01.2026', true])
|
||||
assert.equal(v({ state: 'cancelled', periodEnd: '2027-01-31' }).text, 'gekündigt zum 31.01.2027')
|
||||
assert.equal(v({ state: 'cancelled', periodEnd: null }).text, 'gekündigt')
|
||||
assert.deepEqual([v({ state: 'incomplete' }).text, v({ state: 'incomplete' }).muted], ['Fristen fehlen', true])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/legalView.test.mjs`
|
||||
Expected: FAIL, Modul nicht gefunden.
|
||||
|
||||
- [ ] **Step 3: Implementieren**
|
||||
|
||||
- Die vier Funktionen samt Formatierer `euro` (Z. 8–36 von `ProviderContractsTab.jsx`) wandern unverändert nach `src/lib/legalView.js`.
|
||||
- `deadlineView` ergänzt sie. `text` ist `deadlineLabel(result)`. Bei `deadlineTone(result, today)` gleich `red` ist `badge` `'danger'`, bei `orange` `'warn'`, sonst `null`.
|
||||
- Bei gesetztem `badge` ist `lead` `kündbar bis ${formatDate(noticeBy)}` und `rest` `(Periode endet ${formatDate(periodEnd)})`. Sonst ist `lead` gleich `text` und `rest` leer.
|
||||
- `muted` ist wahr bei Ton `grey` oder `missing`.
|
||||
|
||||
- [ ] **Step 4: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/legalView.test.mjs && "$W/check.sh" --nur-tests`
|
||||
Expected: PASS, Exit 0, Testzahl N0 + 3, `# fail 0`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/legalView.js tests/unit/legalView.test.mjs
|
||||
git commit -m "Rechtliches: Anzeige-Texte der Vertragsliste als Lib mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: `validateEmployeeForm` (Lib, TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/employeeForm.js`
|
||||
- Test: `tests/unit/employeeForm.test.mjs` (neu)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `validateEmployeeForm(form, isEdit) → string | null`. Der String ist der deutsche Fehlertext, `null` heißt gültig. Prüfungen und Texte sind die aus `AdminPage.jsx` Z. 70–82.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
```js
|
||||
const { validateEmployeeForm } = await loadModule('src/lib/employeeForm.js')
|
||||
const base = { displayName: 'Max Muster', email: 'max@webklar.com', password: '', shortcode: '', isAdmin: false }
|
||||
const NEED = 'Name und E-Mail sind erforderlich.'
|
||||
|
||||
test('validateEmployeeForm: neuer Mitarbeiter', () => {
|
||||
assert.equal(validateEmployeeForm({ ...base, displayName: ' ', password: '12345678' }, false), NEED)
|
||||
assert.equal(validateEmployeeForm({ ...base, email: '', password: '12345678' }, false), NEED)
|
||||
assert.equal(validateEmployeeForm({ ...base, password: '1234567' }, false), 'Das Passwort muss mindestens 8 Zeichen haben.')
|
||||
assert.equal(validateEmployeeForm({ ...base, password: '12345678' }, false), null)
|
||||
})
|
||||
|
||||
test('validateEmployeeForm: Mitarbeiter bearbeiten', () => {
|
||||
assert.equal(validateEmployeeForm(base, true), null) // leeres Passwort = unverändert
|
||||
assert.equal(validateEmployeeForm({ ...base, password: 'abc' }, true), 'Ein neues Passwort muss mindestens 8 Zeichen haben.')
|
||||
assert.equal(validateEmployeeForm({ ...base, password: 'abcdefgh' }, true), null)
|
||||
assert.equal(validateEmployeeForm({ ...base, email: ' ' }, true), NEED)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/employeeForm.test.mjs`
|
||||
Expected: FAIL, Modul nicht gefunden.
|
||||
|
||||
- [ ] **Step 3: Implementieren**
|
||||
|
||||
Reihenfolge der Prüfungen: erst Name und E-Mail (getrimmt), dann bei `!isEdit` die Mindestlänge 8, dann bei `isEdit` mit nicht leerem Passwort die Mindestlänge 8.
|
||||
|
||||
- [ ] **Step 4: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/employeeForm.test.mjs && "$W/check.sh" --nur-tests`
|
||||
Expected: PASS, Exit 0, Testzahl N0 + 5, `# fail 0`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/employeeForm.js tests/unit/employeeForm.test.mjs
|
||||
git commit -m "Admin: Pruefung des Mitarbeiter-Formulars als Lib mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 4: Werkzeug – Testdaten, Szenario, Ansichten, Gate `p8`
|
||||
|
||||
**Files** (alle unter `$W/`, kein Commit im Repo):
|
||||
- Create: `fixtures/lang-recht-admin.mjs`
|
||||
- Modify: `backend.mjs`, `selftest.mjs`, `checks.mjs`, `views.mjs`, `gates.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createBackend`, `VIEWS`, `GATES` und die Testdaten aus P0.
|
||||
- Produces: die Ansichten aus Step 4, `GATES.p8`, das Szenario `'mitarbeiter'` und die erweiterte Konsolen-Ausnahme des Szenarios `fehler`.
|
||||
|
||||
- [ ] **Step 1: Testdaten `lang-recht-admin` anlegen**
|
||||
|
||||
Die Datei erweitert `standard` im Stil von `altdaten.mjs`. Alle Daten sind relativ zum heutigen Tag berechnet, damit die Töne stabil bleiben.
|
||||
- **Rechtstexte:** Dokument A (Titel 50 Zeichen) mit gültiger Version, Entwurf und zwei abgelösten Versionen, eine mit 300-Zeichen-Notiz und eine Datei mit 70-Zeichen-Namen. Dokument B hat nur einen Entwurf, Dokument C keine Version.
|
||||
- **Verträge:** Je ein Vertrag mit Kündigungstermin in 10 Tagen (rot), 60 (orange) und 200 (normal), jeweils Frist 30 Tage, Laufzeit und Verlängerung 12 Monate. Dazu ein gekündigter, beendeter, unvollständiger, jederzeit kündbarer und auslaufender Vertrag. Einer trägt Anbieter (60 Zeichen), Zweck (90), Notiz (300) und eine Datei (70).
|
||||
- **Mitarbeiter:** die 4 Standard-Mitarbeiter plus einer mit Namen (40) und E-Mail (70 Zeichen). Der Mitarbeiter KNSO trägt die `userId` des angemeldeten Kontos, sonst ergänzen (Ansicht `admin-bearbeiten` braucht die eigene Zeile).
|
||||
|
||||
- [ ] **Step 2: Szenario `mitarbeiter` (erst Selbsttest, dann Backend)**
|
||||
|
||||
`selftest.mjs` bekommt die Prüfung: `GET https://ticket.test/v1/account` liefert im Szenario `mitarbeiter` 200 mit `labels: ['mitarbeiter']`.
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && cd "$W" && node selftest.mjs`
|
||||
Expected: FAIL. Nach der Ergänzung in `backend.mjs`: `selftest ok`.
|
||||
|
||||
- [ ] **Step 3: Konsolen-Ausnahme für absichtliche 500er**
|
||||
|
||||
Die absichtlich per `page.route` erzeugten 500er schreibt der Browser als `console.error` („Failed to load resource“). Der Check `console-error` nimmt Meldungen aus, die das Szenario `fehler` auslöst (P0).
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "fehler" "$W/checks.mjs" "$W/shots.mjs"`
|
||||
- Die Ansichten mit absichtlichen 500ern (`dlg-vertrag-fehler`, `dlg-version-fehler`, `admin-fehler`, `admin-ausfall`) laufen im Szenario `fehler`.
|
||||
- Deckt die Ausnahme ihre Meldungen nicht ab, wird sie in `checks.mjs` um die URL-Muster `**/collections/*/documents` (nur POST) und `**/api/admin/employees` erweitert. P7 tut dasselbe für die Finanzen, ein schon vorhandener gleichwertiger Eintrag wird mitbenutzt.
|
||||
|
||||
- [ ] **Step 4: Ansichten in `views.mjs` anlegen oder anpassen**
|
||||
|
||||
Alle nutzen `fixtures: 'lang-recht-admin'`, die vier Fehleransichten zusätzlich `scenario: 'fehler'`. Der `path` ist `/rechtliches` bzw. `/admin`. Die Schritte haben die Signatur `steps(page, ctx)`, `ctx.backend.calls` listet die vom Backend beantworteten Requests. Fehlerantworten setzen die Schritte mit `page.route(…)`. Seitenrouten haben Vorrang vor den Kontextrouten des Backends, `route.fallback()` reicht alles andere durch. Geprüft wird mit `throw new Error(…)`, das den Verstoß `step-failed` erzeugt. Die vier `dlg-*`-Ansichten behalten die Viewports `[[1440, 900], [390, 844], [1440, 700], [390, 700]]` und rufen am Ende `submitReachable(page)` auf (P0).
|
||||
|
||||
| ID | Schritte |
|
||||
|---|---|
|
||||
| `rechtliches` | `/rechtliches`. Fokus auf `.legal-doc-summary` des ersten Dokuments, Enter. Es muss `aria-expanded="true"` stehen und `.legal-history` sichtbar sein. |
|
||||
| `rechtliches-anbieter` | Reiter „Verträge mit Anbietern“. Fokus auf `button.link` der ersten Zeile, Enter. `.legal-detail` muss sichtbar sein. Klick „Als gekündigt markieren“, `.legal-cancel` muss sichtbar sein. |
|
||||
| `dlg-vertrag` | Reiter Anbieter, Klick „Neuer Vertrag“. `[role=dialog]` muss offen sein, Esc muss ihn schließen. Dann wieder öffnen. |
|
||||
| `dlg-vertrag-fehler` | Dialog öffnen, Anbieter „Testanbieter“ eintragen. `POST **/collections/*/documents` mit 800 ms Verzögerung und 500 `{ message: 'Test: Speichern fehlgeschlagen' }` beantworten, Speichern klicken. Sofort muss `.dialog-close` `disabled` sein, Esc darf nicht schließen. Nach der Antwort steht `.msg.err[role=alert]` mit dem Text im offenen Dialog, und „Anbieter“ trägt noch „Testanbieter“. |
|
||||
| `dlg-version` | Reiter Texte, Klick „Neue Version“ am ersten Dokument. Bei 390 px liegt der Speichern-Knopf nach `scrollIntoView` im Fenster. |
|
||||
| `dlg-version-fehler` | Wie `dlg-vertrag-fehler` mit Version „v9“. |
|
||||
| `admin` | `/admin`. |
|
||||
| `admin-neu` | Klick „Mitarbeiter anlegen“. Dialog muss offen sein, Esc schließt ihn. Dann wieder öffnen. |
|
||||
| `admin-fehler` | Dialog öffnen, Name, E-Mail, Passwort „abc“, absenden. Im Dialog muss „Das Passwort muss mindestens 8 Zeichen haben.“ als `.msg.err` stehen. Dann Passwort „12345678“, `POST **/api/admin/employees` mit 500 `{ error: 'Test: Login konnte nicht angelegt werden' }` beantworten, absenden. Die Meldung wechselt, der Dialog bleibt offen. |
|
||||
| `admin-zugang` | Gültiges Formular absenden. Der Dialog muss zu sein, `.msg.ok` und der Abschnitt „Login wurde angelegt“ müssen sichtbar sein. |
|
||||
| `admin-bearbeiten` | Klick „Bearbeiten“ in der Zeile des angemeldeten Kontos. Titel „Mitarbeiter bearbeiten“, Admin-Checkbox `disabled`. |
|
||||
| `admin-ausfall` | Zusätzlich zum Szenario `GET **/api/admin/employees` mit 500 beantworten (die Seite fragt erst das Portal, dann die Collection). Ein `.msg.err` steht auf der Seite, „Mitarbeiter anlegen“ öffnet den Dialog. |
|
||||
| `admin-403` | Szenario `mitarbeiter`. Die Überschrift „Zugriff verweigert“ steht da. |
|
||||
|
||||
- [ ] **Step 5: Gate festlegen**
|
||||
|
||||
In `gates.mjs` kommt nach der Deklaration von `GATES` dazu. Das Gate `p8` erbt Prüfungen und Ansichten von `p7` (die `p1`-Prüfungen für alle Ansichten, `step-failed`, `external-request` und `console-error` immer) und schaltet `icon-svg` zusätzlich für die Ansichten von P8 und aller früheren Pakete ein:
|
||||
|
||||
```js
|
||||
const iconSvg = GATES.p7.scoped['icon-svg']
|
||||
GATES.p8 = {
|
||||
...GATES.p7,
|
||||
scoped: {
|
||||
...GATES.p7.scoped,
|
||||
'icon-svg': {
|
||||
...iconSvg,
|
||||
views: [...iconSvg.views, 'rechtliches', 'rechtliches-anbieter', 'dlg-vertrag', 'dlg-vertrag-fehler', 'dlg-version', 'dlg-version-fehler',
|
||||
'admin', 'admin-neu', 'admin-fehler', 'admin-zugang', 'admin-bearbeiten', 'admin-ausfall', 'admin-403'],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- [ ] **Step 6: Ausgangslauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8-vorher && "$W/shots.sh" p8-vorher "$W/shots/p8-vorher" --only admin --gate p8`
|
||||
Expected: Exit 1 mit `icon-svg` in `admin` (Icons der Zeilenaktionen und des Knopfes). Das ist der Ausgangszustand.
|
||||
|
||||
### Task 5: Rechtstexte-Reiter
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/LegalPage.jsx` (Z. 19 Ladetext, Z. 34 Fehlerzeile)
|
||||
- Modify: `src/components/legal/LegalTextsTab.jsx` (Markup Z. 23–83, Logik Z. 9–21 bleibt)
|
||||
- Modify: `src/components/legal/legalUi.jsx` (`FileLinks`, Z. 8–21)
|
||||
- Modify: `src/styles/global.css` (Block „Rechtliches“, `rg -n "Rechtliches ---" src/styles/global.css`; bei Bedarf das Zurücksetzen für `button.link` aus Task 1)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Card`, `Badge`, `Tabs`, `EmptyState`, `PageHeader` (P1), `.link`, `.msg.err`, `.loading`, `currentVersion` und `formatDate` aus `lib/legal.js`.
|
||||
- Produces: `LegalTextsTab` und `FileLinks` behalten ihre Props. Dateilinks tragen `class="link"`, der Download-Link zusätzlich `legal-dl`.
|
||||
|
||||
- [ ] **Step 1: `LegalPage.jsx` anpassen**
|
||||
|
||||
- `<div className="faint">Lädt …</div>` wird `<div className="loading">Lädt …</div>`. `<div className="legal-error">` wird `<div className="msg err" role="alert">`.
|
||||
|
||||
Entscheidung: Die Ladetexte „Lädt …“ und (Task 9) „Mitarbeiter werden geladen …“ bleiben wörtlich. Die Spec ändert nur „Loading...“, hier ändert sich nur die Klasse.
|
||||
|
||||
- [ ] **Step 2: `LegalTextsTab.jsx` umbauen**
|
||||
|
||||
- Wurzel `<div className="legal-docs">`. Der Aktionsfehler wird `<div className="msg err legal-msg" role="alert">`.
|
||||
- Pro Dokument `Card className="legal-doc" title={doc.title}` (ein eigener Abschnitt, den Abstand zum nächsten liefert P1). Die Aktion ist `<button type="button" className="link" onClick={() => setFormFor(doc)}>Neue Version</button>`.
|
||||
- Die Zusammenfassung ist ein echter `<button type="button" className="legal-doc-summary" aria-expanded={expanded}>` statt `div role="button"`. Der Inhalt bleibt: Badge „{version} · gilt seit …“ bzw. „noch keine gültige Version“, Badges „Entwurf {version}“, dazu `<span className="legal-toggle">` mit „Verlauf ausblenden“ bzw. „Verlauf (n)“.
|
||||
- „Live-Seite öffnen“ ist `<a className="link legal-live" …>`.
|
||||
- Verlauf `ol.legal-history`, je Version ein `li` mit der Klasse `legal-version`, bei Status `superseded` zusätzlich `is-superseded` (kein Klassenname mehr aus dem Status). Kopf: Version fett, Zeitraum in `<span className="muted">`, Badges „gültig“ (ok) und „Entwurf“ (muted), „Als gültig markieren“ als `<button type="button" className="link">`.
|
||||
- „Noch keine Versionen eingetragen.“ bleibt `.faint`, `VersionForm` wird wie bisher gerendert.
|
||||
|
||||
- [ ] **Step 3: `legalUi.jsx` anpassen**
|
||||
|
||||
Dateilink `<a className="link" …>`, Download `<a className="link legal-dl" …>(herunterladen)</a>`. „keine Datei“ bleibt `.faint` (leerer Zustand).
|
||||
|
||||
- [ ] **Step 4: CSS im Block „Rechtliches“ ersetzen**
|
||||
|
||||
Gelöscht werden `.legal-grid` samt `> .legal-error`, `.legal-version-current`, `-draft`, `-superseded` und `.legal-files a`. Neu oder geändert:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.legal-doc-summary` | `display: flex; flex-wrap: wrap; align-items: center; gap: 6px; width: 100%; padding: 12px 0; text-align: left; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line)` (Knopf-Reset kommt aus der Basis) |
|
||||
| `.legal-toggle` | `margin-left: auto; font-size: 13px; color: var(--text-2)` |
|
||||
| `.legal-live` | `display: inline-block; margin-top: 8px; font-size: 13px` |
|
||||
| `.legal-history` | `list-style: none; margin: 0; padding: 0` |
|
||||
| `.legal-version` | `padding: 10px 0; border-bottom: 1px solid var(--line)` |
|
||||
| `.legal-version.is-superseded` | `color: var(--text-2)` |
|
||||
| `.legal-version-head` | `display: flex; flex-wrap: wrap; align-items: center; gap: 8px` |
|
||||
| `.legal-version-head .link` | `margin-left: auto` |
|
||||
| `.legal-note` | `white-space: pre-wrap; margin: 4px 0; font-size: 13px; color: var(--text-2); overflow-wrap: anywhere` |
|
||||
| `.legal-files` | `display: inline-flex; flex-wrap: wrap; gap: 4px 12px; font-size: 13px; overflow-wrap: anywhere` |
|
||||
| `.legal-files .link` | `font-weight: 400` |
|
||||
| `.legal-files .legal-dl` | `color: var(--text-2)` |
|
||||
| `.legal-msg` | `margin: 0 0 12px` |
|
||||
|
||||
- [ ] **Step 5: Bauen und prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t5" --only rechtliches --gate p8`
|
||||
Expected: PASS. In den Bildern gilt:
|
||||
- Kein Kasten um Dokumente, nur Haarlinien über und unter der Zusammenfassung und zwischen den Versionszeilen.
|
||||
- „Neue Version“ steht als Textlink rechts neben der Überschrift.
|
||||
- Die Zeile der gültigen Version hat keinen grünen Hintergrund (nur das Badge ist grün), die des Entwurfs keinen gestrichelten Rand.
|
||||
- Dateilinks sind unterstrichen. Bei 390 px läuft nichts über.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/LegalPage.jsx src/components/legal/LegalTextsTab.jsx src/components/legal/legalUi.jsx src/styles/global.css
|
||||
git commit -m "Design: Rechtstexte als Abschnitte mit Haarlinien, Aufklappzeile als Button" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Anbieter-Reiter
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/legal/ProviderContractsTab.jsx` (lokale Label-Funktionen Z. 8–36 löschen, Markup Z. 75–140)
|
||||
- Modify: `src/styles/global.css` (Block „Rechtliches“)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `costLabel`, `termLabel`, `noticeLabel`, `deadlineView` (Task 2), `isRowActivation(event)` aus `src/lib/rowClick.js` (P2 Task 2, Prüfung in Task 1), `deadlineTone`, `nextCancellationDate` (`lib/legal.js`), `FileLinks`, `ContractForm` (Props unverändert), `Badge`, `Button`, `EmptyState`.
|
||||
- Produces: `ProviderContractsTab` behält seine Props.
|
||||
|
||||
- [ ] **Step 1: Markup umbauen**
|
||||
|
||||
`TONE_ORDER`, Sortierung, `run`, `confirmCancel` und `remove` (Z. 38–73) bleiben.
|
||||
- Aktionsfehler `<div className="msg err legal-msg" role="alert">`. Die `.toolbar` mit „Neuer Vertrag“ (`Button variant="primary"`) bleibt.
|
||||
- `<table className="table legal-contracts">`. Die Spalten „Wofür“, „Kosten“, „Laufzeit“ und „Kündigungsfrist“ bekommen `hide-sm`.
|
||||
- Zeile ohne `legal-tone-*`: `<tr className="row" onClick={(e) => { if (isRowActivation(e)) setOpen(open === c.$id ? null : c.$id) }}>`. Der Helfer ignoriert Klicks auf `a, button, select, input, label`.
|
||||
- Erste Zelle: `<div className="cell-title"><button type="button" className="link" aria-expanded={open === c.$id} onClick={() => setOpen(open === c.$id ? null : c.$id)}>{c.provider}</button></div>`.
|
||||
- Button und Zeilenhandler benutzen denselben Umschalter `setOpen(open === c.$id ? null : c.$id)`.
|
||||
- Der Button braucht seinen eigenen `onClick`, weil `isRowActivation` Klicks auf `button` ausfiltert. Ohne ihn klappt weder Klick noch Enter auf dem Button etwas auf, und die Ansicht `rechtliches-anbieter` scheitert.
|
||||
- Zelle „Nächster Termin“ mit `view = deadlineView(result, today)`: Bei `view.badge` steht `<Badge tone={view.badge}>{view.lead}</Badge>` und dahinter `<span className="muted"> {view.rest}</span>`. Sonst steht `<span className={view.muted ? 'muted' : undefined}>{view.text}</span>`.
|
||||
- Detail-Zeile `tr.legal-detail > td colSpan={6}`, von oben nach unten:
|
||||
- `dl.kv.legal-facts` mit „Wofür“, „Kosten“, „Laufzeit“, „Kündigungsfrist“, nur unter 860 px sichtbar.
|
||||
- „Beginn: …“, Notiz `.legal-note`, `FileLinks`.
|
||||
- `div.legal-actions`: `button.link` „Bearbeiten“, `button.link` „Als gekündigt markieren“ (Bedingung unverändert), `Button variant="danger"` „Löschen“.
|
||||
- Beim Kündigen steht statt des Links `span.legal-cancel`: ``<label htmlFor={`cancel-${c.$id}`}>Endet am</label>``, `<input id=… type="date" className="input">`, `Button variant="primary"` „Speichern“, `Button` „Abbrechen“.
|
||||
|
||||
Entscheidung: Die Aufklappzeile folgt der Tabellenregel der Spec. Der echte Button in der ersten Zelle trägt die Bedienung, der Zeilenklick ist nur eine Zugabe für die Maus.
|
||||
|
||||
Entscheidung: Unter 860 px zeigt die Tabelle nur „Anbieter“ und „Nächster Termin“ (Kernspalten-Regel). Die übrigen Angaben stehen dann im Detail (`.legal-facts`) mit den vorhandenen Spaltenköpfen als Beschriftung. Es kommt kein neuer Text dazu, und keine Information geht verloren.
|
||||
|
||||
- [ ] **Step 2: CSS ergänzen und Altregeln löschen**
|
||||
|
||||
Gelöscht werden `.legal-tone-*`, das alte `.legal-detail td` und der `@media (max-width: 700px)`-Block. Neu oder geändert:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.legal-detail td` | `padding: 0 0 16px` (ohne Hintergrund) |
|
||||
| `.legal-facts` | `display: none; margin-bottom: 8px`, unter 860 px `display: grid` |
|
||||
| `.legal-actions` | `display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 8px` |
|
||||
| `.legal-cancel` | `display: flex; flex-wrap: wrap; align-items: center; gap: 8px` |
|
||||
| `.legal-cancel .input` | `width: 160px` |
|
||||
|
||||
- [ ] **Step 3: Bauen und prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t6" --only rechtliches-anbieter --gate p8`
|
||||
Expected: PASS. In den Bildern gilt:
|
||||
- Keine getönte Zeile. Bei rot und orange steht im Terminfeld ein Badge „kündbar bis …“ mit dem Rest in Grau, beendete und gekündigte Verträge haben grauen Text im Terminfeld.
|
||||
- Das Detail zeigt Textlinks, das Kündigungs-Formular steht in einer Zeile.
|
||||
- Bei 390 px gibt es zwei Spalten, das Detail zeigt die vier Angaben als `.kv`.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/legal/ProviderContractsTab.jsx src/styles/global.css
|
||||
git commit -m "Design: Anbieter-Vertraege als Tabelle, Dringlichkeit nur im Terminfeld" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 7: Vertrags- und Versions-Dialog
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/legal/ContractForm.jsx` (Markup Z. 56–105, Import Z. 2)
|
||||
- Modify: `src/components/legal/VersionForm.jsx` (Markup Z. 26–63, Import Z. 2)
|
||||
- Modify: `src/styles/global.css` (Block „Rechtliches“)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Dialog`, `Field`, `Button` (P1), `useScrollIntoView(ref, trigger)` aus `src/hooks/useScrollIntoView.js`.
|
||||
- Produces: `ContractForm({ contract, onSave, onClose })` und `VersionForm({ title, onSave, onClose })` behalten ihre Signaturen.
|
||||
|
||||
- [ ] **Step 1: `ContractForm.jsx` umbauen**
|
||||
|
||||
`FIELDS`, `NUMBERS`, `toForm`, `toDoc`, `set` und `submit` bleiben (Fehler landet weiter in `error`, Erfolg ruft `onClose()`).
|
||||
- Der `FaTimes`-Import entfällt. Neu sind `errorRef = useRef(null)` und `useScrollIntoView(errorRef, error)`.
|
||||
- Gerüst: `<Dialog as="form" wide title={contract ? 'Vertrag bearbeiten' : 'Neuer Vertrag'} busy={saving} onClose={onClose} onSubmit={submit} footer={…}>`.
|
||||
- Felder als `Field label htmlFor` mit `id="contract-<key>"`. Eingaben tragen `className="input"`, Auswahlen `select`, die Notiz `textarea`. `name`, `required`, `type`, `min`, `inputMode` und `accept` bleiben.
|
||||
- Paare in `<div className="field-row">`: Kosten und „pro“, Laufzeit und „Verlängert sich um“, Kündigungsfrist und „Einheit“.
|
||||
- Upload: `<input type="file" className="legal-file" multiple accept=…>` in `Field label="Dateien hinzufügen"`.
|
||||
- Der Fehler ist das erste Kind: `{error && <div className="msg err" role="alert" ref={errorRef}>{error}</div>}`.
|
||||
- Fuß: `<span className="grow" />`, `<Button type="button" onClick={onClose} disabled={saving}>Abbrechen</Button>`, `<Button variant="primary" type="submit" disabled={saving}>{saving ? 'Speichert …' : 'Speichern'}</Button>`.
|
||||
|
||||
Entscheidung: „Abbrechen“ ist während des Speicherns gesperrt, wie „ד und Esc. Heute schließt es mitten im Speichern und verwirft die Rückmeldung.
|
||||
|
||||
- [ ] **Step 2: `VersionForm.jsx` umbauen**
|
||||
|
||||
Gleiches Muster, auch im Fuß (`Abbrechen` mit `type="button"`, Absenden mit `type="submit"`). Der Titel ist `Neue Version: ${title}`, der Dialog nicht `wide`, die IDs sind `version-<key>`. „Version“ behält `placeholder="z. B. v6"` und `required`, „Gültig ab“ bleibt `required`, die Notiz `rows={4}`, der Upload bekommt `legal-file`.
|
||||
|
||||
- [ ] **Step 3: CSS**
|
||||
|
||||
Gelöscht werden `.legal-error` und `.legal-form-row`. Neu:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.legal-file` | `font-size: 13px; color: var(--text-2)` |
|
||||
| `.legal-file::file-selector-button` | `height: 28px; padding: 0 10px; margin-right: 10px; border: 0; border-radius: var(--radius); background: var(--bg); box-shadow: var(--ring); font: inherit; font-weight: 500; color: var(--text); cursor: pointer` |
|
||||
|
||||
- [ ] **Step 4: Hygiene und Bilder**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "overlay|react-icons|form-control|form-group|form-label|legal-error|legal-form-row|ui-btn" src/components/legal src/pages/LegalPage.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t7" --only dlg-vertrag,dlg-vertrag-fehler,dlg-version,dlg-version-fehler --gate p8`
|
||||
Expected: PASS, das heißt:
|
||||
- Esc schließt den Dialog. Bei laufendem Speichern ist „ד gesperrt und Esc wirkungslos.
|
||||
- Nach dem Fehler steht `.msg.err` im offenen Dialog, die Eingabe ist erhalten.
|
||||
- Bei 390 px ist der Dialog ein Bottom-Sheet, und der Speichern-Knopf ist erreichbar.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/legal/ContractForm.jsx src/components/legal/VersionForm.jsx src/styles/global.css
|
||||
git commit -m "Design: Vertrags- und Versions-Formular als Dialoge, Fehler im Dialog" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 8: Zugangsdaten-, `sr-only`- und `spin`-Klassen nach `global.css`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/global.css` (Ende von Block 3 „Bausteine“, vor `/* ===== KOMPAT (bis P10) ===== */`)
|
||||
- Modify: `src/pages/AdminPage.css` (Blöcke `.credential*`, `.sr-only`, `.spin` samt `@keyframes admin-spin` löschen)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `src/components/CopyableCredential.jsx` in der Fassung von P5 (Textknöpfe „Anzeigen“/„Verbergen“, „Kopieren“/„Kopiert“).
|
||||
- Produces: die Klassen für `CopyableCredential` und `.sr-only` für alle Seiten.
|
||||
|
||||
- [ ] **Step 1: Klassen der Komponente lesen**
|
||||
|
||||
Run: ``source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -o 'className=\{?[`"][^`"]*credential[^`"]*' src/components/CopyableCredential.jsx | sort -u``
|
||||
Expected: `credential`, `credential-value`, `is-masked`, `credential-empty`. P5 hat `credential-action` und `credential-check` gelöscht, die Knöpfe sind `button.link` in `.credential`. Es werden nur die Regeln verschoben, die `design-p7:src/pages/AdminPage.css` noch enthält.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git show design-p7:src/pages/AdminPage.css`
|
||||
Expected: Zu sehen sind die verbliebenen Blöcke `.credential*`, `.sr-only`, `.spin` und `@keyframes admin-spin`. Nur diese werden verschoben.
|
||||
|
||||
- [ ] **Step 2: Regeln nach `global.css` verschieben und neu einfärben**
|
||||
|
||||
| Regel | Änderung beim Verschieben |
|
||||
|---|---|
|
||||
| `.credential`, `.credential .link`, `.credential-value.is-masked` | unverändert (Stand P5: `gap: 8px`, `.link` mit `flex: 0 0 auto; font-size: 12px; white-space: nowrap`) |
|
||||
| `.credential-value` | `overflow: hidden; text-overflow: ellipsis` wird `overflow-wrap: anywhere`, sonst unverändert. Lange Werte brechen um, statt abgeschnitten zu werden und die Admin-Tabelle bei 390 px zu sprengen. |
|
||||
| `.credential-empty` | `color: var(--text-3)` (steht für „–“) |
|
||||
| `.sr-only` | unverändert (`position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0`) |
|
||||
| `.spin` | `animation: spin .8s linear infinite` (nutzt die vorhandenen `@keyframes spin`, der Block `@keyframes admin-spin` entfällt) |
|
||||
|
||||
Entscheidung: `.spin` zieht wie in der Spec nach `global.css`, obwohl es nach P8 keinen Aufrufer mehr hat. Der einzige war der Spinner der Admin-Seite. P10 löscht es, wenn `rg -w spin src --glob '!*.css'` dann weiter nichts findet. Das steht in der Übergabe.
|
||||
|
||||
- [ ] **Step 3: Prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.undefinedUsed.filter(n=>/credential|sr-only/.test(n)).join('\n'))"`
|
||||
Expected: leer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t8" --only admin,kunde-overview --gate p7`
|
||||
Expected: PASS. Das Gate `p7` ist das des Vorgängerpakets, denn `icon-svg` für `admin` wird erst mit Task 9 sauber. Zugangsdaten in der Admin-Tabelle und auf der Kundenseite (Ansicht `kunde-overview`, Abschnitt „Portal-Zugang“) sind lesbar, die Textknöpfe „Kopieren“ und „Anzeigen“ unterstrichen.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/styles/global.css src/pages/AdminPage.css
|
||||
git commit -m "Design: Zugangsdaten-, sr-only- und spin-Klassen nach global.css" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 9: AdminPage
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/AdminPage.jsx` (Importe Z. 1–13, Zustand Z. 40–45, 403-Block Z. 52–62, Formular-Logik Z. 64–112, Markup Z. 123–226; `showNotice` und `removeEmployee` Z. 114–121 bleiben)
|
||||
- Rewrite: `src/pages/AdminPage.css`
|
||||
- Modify: `src/components/AdminRoute.jsx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `validateEmployeeForm(form, isEdit)` (Task 3), `Dialog`, `Field`, `Button`, `Badge`, `Card`, `PageHeader`, `Tabs`, `EmptyState` (P1), `CopyableCredential` (P5), `useScrollIntoView`, `useEmployees` und `useAuth` unverändert.
|
||||
- Produces: nichts für spätere Pakete.
|
||||
|
||||
- [ ] **Step 1: Seitengerüst**
|
||||
|
||||
- Alle Icon-Importe entfallen. `Notice` rendert ``<div className={`msg ${notice.type === 'error' ? 'err' : 'ok'} admin-notice`} role={notice.type === 'error' ? 'alert' : 'status'}>``.
|
||||
- Kopf: `PageHeader eyebrow="Systemverwaltung" title="Admin" subtitle="Mitarbeiter und ihre Zugänge verwalten. Kunden pflegst du unter „Kunden“."`. Die Aktionen sind `<span className="muted small">Angemeldet als <strong>{user?.name || user?.email}</strong></span>` und `Button variant="primary"` „Mitarbeiter anlegen“, der einzige schwarze Knopf im Kopf.
|
||||
- Der Reiter ist `Tabs` mit einem Eintrag. Das Label bleibt `Mitarbeiter (${employees.length})` ohne `count`, weil `count` den Text ändern würde.
|
||||
- Die Wurzel bleibt `<div className="page admin-page">`. Der 403-Block behält sein Markup (`span` „403“, `h1` „Zugriff verweigert“, `p`).
|
||||
|
||||
- [ ] **Step 2: Zugangsdaten-Abschnitt**
|
||||
|
||||
`createdCredential` rendert `Card title="Login wurde angelegt" actions={<button type="button" className="btn btn-quiet" aria-label="Hinweis schließen">×</button>}` mit `<p className="muted small">Diese Zugangsdaten jetzt sicher weitergeben.</p>` und `<div className="credential-list">` um die zwei `CopyableCredential` (E-Mail, Passwort mit `secret`). Er steht über der Tabelle und ist kein Dialog.
|
||||
|
||||
- [ ] **Step 3: Tabelle**
|
||||
|
||||
`Card title="Mitarbeiterkonten"` mit `<p className="muted small admin-lead">Aktive Logins für das Ticketsystem</p>`:
|
||||
- Ladefehler `employeesError`: `div.msg.err[role=alert]`.
|
||||
- Laden: `<div className="loading">Mitarbeiter werden geladen …</div>`.
|
||||
- Leer: `EmptyState title="Noch keine Mitarbeiter" hint="Lege mit „Mitarbeiter anlegen“ den ersten Login an."`.
|
||||
|
||||
Entscheidung: Der Hinweis „Lege links den ersten Login an.“ passt nicht mehr, weil das Formular nicht mehr links steht. Das ist die einzige Textänderung außerhalb der Spec-Liste, die Übergabe nennt sie.
|
||||
|
||||
Die Tabelle ist `<table className="table">` ohne `tr.row`, denn die Zeilen sind heute auch nicht klickbar. Spalten:
|
||||
|
||||
| Spalte | Inhalt | Klassen |
|
||||
|---|---|---|
|
||||
| Mitarbeiter | `strong` mit Name oder „–“ | – |
|
||||
| E-Mail | `CopyableCredential` (Label „E-Mail“) | – |
|
||||
| Kürzel | `<span className="admin-shortcode">` | `hide-sm` |
|
||||
| Rolle | `Badge tone="ok"` „Admin“, sonst `<span className="muted">Mitarbeiter</span>` | `hide-sm` |
|
||||
| User-ID | `CopyableCredential` (Label „User-ID“) | `hide-sm` |
|
||||
| Aktionen (`sr-only`) | `div.admin-actions` mit `button.link` „Bearbeiten“ und `Button variant="danger"` „Löschen“, Letzteres nur wenn `employee.userId !== user?.$id` | `right` |
|
||||
|
||||
Die `aria-label` der Aktionen bleiben `${employee.displayName} bearbeiten` und `… löschen`. Auf dem Handy bleiben Mitarbeiter, E-Mail und Aktionen.
|
||||
|
||||
- [ ] **Step 4: Formular als Dialog**
|
||||
|
||||
- Neu sind `formOpen`, `formError`, `errorRef` und `useScrollIntoView(errorRef, formError)`. `editingSelf` ist die einmal berechnete Bedingung „Admin-Haken gehört zum eigenen Konto“ (heute zweimal inline).
|
||||
- „Mitarbeiter anlegen“ und „Bearbeiten“ setzen Formular und `editingEmployee`, leeren `formError` und öffnen mit `setFormOpen(true)`.
|
||||
- `submitEmployee` prüft zuerst `validateEmployeeForm(employeeForm, Boolean(editingEmployee))`. Bei einem Text setzt es `formError` (kein Banner) und kehrt zurück.
|
||||
- Sonst läuft der Ablauf wie bisher: `employeeBusy` an, `updateEmployee` bzw. `createEmployee`, `employeeBusy` aus.
|
||||
- Bei Misserfolg steht `result.error || 'Mitarbeiter konnte nicht gespeichert werden.'` in `formError`, der Dialog bleibt offen.
|
||||
- Bei Erfolg folgen `setCreatedCredential` (nur beim Anlegen), `showNotice('success', …)` mit den bisherigen Texten, dann Formular schließen und zurücksetzen.
|
||||
- Gerüst: `<Dialog as="form" title={editingEmployee ? 'Mitarbeiter bearbeiten' : 'Mitarbeiter anlegen'} busy={employeeBusy} onClose={closeForm} onSubmit={submitEmployee} footer={…}>`.
|
||||
- Inhalt von oben nach unten:
|
||||
- Fehler `div.msg.err[role=alert][ref=errorRef]`, dann `<p className="muted small">Die User-ID wird automatisch vergeben.</p>`.
|
||||
- Vier `Field` mit `id="employee-<key>"` und `className="input"`: Name (`required`), E-Mail (`type="email"`, `required`), Passwort (Label „Neues Passwort (optional)“ bzw. „Passwort“, `autoComplete="new-password"`, `required={!editingEmployee}`, Platzhalter „Mindestens 8 Zeichen“), Kürzel (`maxLength={10}`, Platzhalter „z. B. KNSO“, Großschreibung beim Tippen).
|
||||
- Admin-Checkbox: ``<label className={`check admin-check${editingSelf ? ' is-disabled' : ''}`}>`` mit `<input type="checkbox" disabled={editingSelf}>`, `<strong>Admin-Account</strong>` und `<small>Darf Systemkonfiguration und Benutzer verwalten.</small>`.
|
||||
- Fuß: `<span className="grow" />`, `Button type="button" disabled={employeeBusy}` „Abbrechen“, `Button variant="primary" type="submit" disabled={employeeBusy}` mit „Speichert …“, „Änderungen speichern“ oder „Mitarbeiter anlegen“.
|
||||
|
||||
- [ ] **Step 5: `AdminPage.css` neu schreiben**
|
||||
|
||||
Die Datei wird vollständig ersetzt:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.page.admin-page` | `max-width: 1480px` |
|
||||
| `.admin-notice` | `margin-bottom: 16px` |
|
||||
| `.admin-lead` | `margin: 0 0 8px` |
|
||||
| `.admin-shortcode` | `font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; letter-spacing: .04em` |
|
||||
| `.admin-actions` | `display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px 16px` |
|
||||
| `.credential-list` | `display: flex; flex-wrap: wrap; gap: 8px 32px; margin-top: 8px` |
|
||||
| `.check.admin-check` | `align-items: flex-start` (Spezifität 0,2,0, sonst gewinnt `.check` aus `global.css`) |
|
||||
| `.admin-check small` | `display: block; margin-top: 2px; font-size: 13px; line-height: 18px; color: var(--text-2)` |
|
||||
| `.admin-check.is-disabled` | `cursor: not-allowed; opacity: .55` |
|
||||
| `.admin-access-denied` | `max-width: 540px; margin: 12vh auto 0` |
|
||||
| `.admin-access-denied > span` | `font-size: 12px; font-weight: 500; color: var(--red-fg)` |
|
||||
| `.admin-access-denied h1` | `margin: 4px 0 8px; font-size: 28px; line-height: 34px; font-weight: 700; letter-spacing: -.02em` |
|
||||
| `.admin-access-denied p` | `color: var(--text-2)` |
|
||||
|
||||
Weg sind alle `!important`, Kästen, Rahmen, Schatten, Farbliterale, der Split-Layout-Block und die Media-Queries. Eine Regel für den Abstand zwischen Zugangsdaten und Mitarbeiterkonten gibt es nicht, ihn liefert `.page section + section` aus P1.
|
||||
|
||||
- [ ] **Step 6: `AdminRoute.jsx` angleichen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n 'className="loading"' src/components/AdminRoute.jsx`
|
||||
- Trifft es, hat ein früheres Paket die Datei schon umgestellt, und dieser Schritt entfällt.
|
||||
- Sonst wird der Ladezweig `<div className="loading"><p>Wird geladen …</p></div>`. Das `<p>` bleibt ohne Klasse, weil `tests/unit/adminRoute.test.mjs` genau diese Zeichenfolge prüft. Der Seitenabstand kommt von `.loading`, Inline-Style, `spinner` und die Hilfsklassen entfallen.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/adminRoute.test.mjs`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Bauen und prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|style=\{|#[0-9a-fA-F]{3,8}\b|!important|admin-icon-button|admin-primary-button|form-control|spinner" src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8-t9" --only admin,admin-neu,admin-fehler,admin-zugang,admin-bearbeiten,admin-ausfall,admin-403 --gate p8`
|
||||
Expected: PASS. In den Bildern gilt:
|
||||
- Keine Karten, die Tabelle hat nur Haarlinien, Aktionen sind Textlinks.
|
||||
- Der Dialog zeigt Validierungs- und Speicherfehler als `.msg.err` oben und schließt nur bei Erfolg.
|
||||
- Nach Erfolg steht das Banner auf der Seite, darunter der Zugangsdaten-Abschnitt.
|
||||
- Bei 390 px sind Mitarbeiter, E-Mail und Aktionen sichtbar, ohne waagerechtes Scrollen.
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/AdminPage.jsx src/pages/AdminPage.css src/components/AdminRoute.jsx
|
||||
git commit -m "Design: Admin mit Tabelle, Mitarbeiter-Formular als Dialog, AdminPage.css neu" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 10: Gate P8, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0`. Die Testzahl ist N0 + 5. Liegen neue Commits auf `test`, kann sie höher sein, die Zahl wird notiert. So zeigen die Bilder der folgenden Schritte den Stand, der getaggt wird.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p8 && "$W/shots.sh" p8 "$W/shots/p8" --gate p8`
|
||||
Expected: Exit 0. Das Gate `p8` prüft alle Ansichten mit den `p1`-Prüfungen (`overflow-x`, `dark-surface`, `low-contrast`) und `icon-svg` für die Ansichten von P2 bis P8. Die 13 P8-Ansichten haben Bilder bei 1440 und 390 px ohne Verstöße.
|
||||
|
||||
- [ ] **Step 3: Hygiene und unveränderte Logik**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{|#[0-9a-fA-F]{3,8}\b|!important|overlay-close|overlay-content|form-control|ui-btn|ui-table" src/pages/AdminPage.jsx src/pages/AdminPage.css src/pages/LegalPage.jsx src/components/legal`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff --stat design-p7 -- src/hooks src/lib/legal.js; git diff design-p7 -- src | rg '^[-+].*window\.confirm'; rg -n "legal-tone|legal-version-(current|draft)|legal-grid|legal-form-row|legal-error|legal-doc \+|admin-page section" src`
|
||||
Expected: alle drei Ausgaben leer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.filter(n=>/^(legal|admin|credential)-/.test(n)).join('\n'))"`
|
||||
Expected: leer.
|
||||
|
||||
- [ ] **Step 4: Bundle-Größe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
|
||||
Expected: Exit 0, CSS kleiner als bei Task 1. Die Werte werden notiert.
|
||||
|
||||
- [ ] **Step 5: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p7..HEAD` gegen die Spec-Abschnitte „Bausteine“ (Dialog, Tabelle, Badge, Meldungen), „Texte“ und Paket P8. Wichtige Befunde werden behoben. Danach kommen Step 1 (Rebase) noch einmal und Step 2 als Kurzlauf mit `--only rechtliches,rechtliches-anbieter,admin`. Schwerpunkte:
|
||||
- Der Dialog schließt nie bei einem Fehler, und „ד, Esc und „Abbrechen“ sind bei `busy` gesperrt.
|
||||
- Die Checkliste der Bedienelemente aus „Review Focus“ ist vollständig.
|
||||
- Die Bedingung „eigene Zeile“ (kein „Löschen“, Admin-Haken gesperrt) gilt weiter.
|
||||
- Die Dringlichkeit steht nur im Terminfeld.
|
||||
- Die Aufklappzeile der Verträge öffnet per Klick auf den Button, per Enter und per Klick auf die Zeile.
|
||||
|
||||
- [ ] **Step 6: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p8 && git log --oneline origin/test..design-p8 && git rev-parse design-p8`
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- die Screenshots aus `$W/shots/p8` (Unterordner `1440x900` und `390x844`; Auswahl: `rechtliches`, `rechtliches-anbieter`, `dlg-vertrag-fehler`, `admin`, `admin-neu`, `admin-fehler`, `admin-zugang`)
|
||||
- den Hinweis auf die Textänderungen: „Bearbeiten“/„Löschen“, „Speichert …“ und der Leer-Hinweis der Admin-Tabelle
|
||||
- die Notiz für P10: `.spin` hat keinen Aufrufer mehr
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p8^{commit}:refs/heads/test'`
|
||||
- die Rücknahme `git revert --no-commit design-p7..design-p8`, Commit und Push durch den Eigentümer (gilt nur, solange P8 das jüngste Paket auf `test` ist)
|
||||
|
||||
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers: Rechtliches mit beiden Reitern und Admin mit der Tabelle. Dabei wird nichts gespeichert oder gelöscht.
|
||||
@@ -0,0 +1,390 @@
|
||||
# WOMS „Weißes Design“ P9: Serverpuls – Umsetzungsplan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Nach P9 ist Serverpuls hell und kastenlos. Der Kopf trägt ein Text-Badge statt eines Emojis, Warnungen sind Listenzeilen mit Stufen-Badge, die Kacheln sind `.stats` mit Sparkline. Deltas zeigen sich als Textfarbe, Umschalter sind `.seg`, Panels sind `h2.section`, die Projekttabelle ist eine `.table`. Die Diagramme nutzen die Farben aus `chartColors.js`. Alle `data-testid` bleiben.
|
||||
|
||||
**Architecture:**
|
||||
- Drei reine Helfer in `src/lib/serverpulsFormat.js` mit Tests: `deltaDir`, `deltaSign`, `levelTone`.
|
||||
- Bausteine aus P1: `Stat`, `Badge` (`badgeToneClass` aus `ui/tones.js`), `PageHeader`, `.stats`, `.list-row`, `.seg`, `.table`, `.msg`, `.toolbar`, `.input.search`.
|
||||
- `serverpuls.css` wird neu geschrieben (74 → rund 70 Zeilen).
|
||||
- `Chart.jsx` und `Sparkline.jsx` bleiben unverändert, weil P1 die Diagrammfarben schon umgestellt hat. Dasselbe prüft Task 6.
|
||||
- Abfragen, 60-Sekunden-Takt, Neuladen bei Sichtbarkeit, `reqId`-Schutz, Sortierlogik und Zeitachsen bleiben unverändert.
|
||||
|
||||
**Tech Stack:** React 18, uPlot (Canvas), reines CSS. Tests mit `node --test`, Screenshot-Werkzeug aus P0.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-04-weisses-design-design.md`, Abschnitte „Gestaltungsregeln“, „Design-Tokens“ (Diagrammfarben), „Bausteine“ (Kennzahlen, Umschalter, Tabelle, Liste, Badge, Meldungen, Laden) und Paket P9. Anhang `2026-10-04-weisses-design-umbaukarte.md`: Abschnitt 5 (P9) und 7.1. Globale Regeln und Schnittstellen: `2026-10-05-weisses-design-00-uebersicht.md`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
Es gelten die Regeln der Übersicht. Für P9 kommt hinzu:
|
||||
|
||||
- Jeder Run-Block beginnt mit `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh`. Danach gelten `$WT` (Worktree, Arbeitsverzeichnis), `$W` (Werkzeug) und `$TMPDIR`. Tests und Build werden nur über `"$W/check.sh"` ausgewertet, Browser-Skripte laufen nur über `"$W/shots.sh"`.
|
||||
- Ausgangspunkt ist das Tag `design-p8`, alle Diff-Prüfungen vergleichen gegen `design-p8`.
|
||||
- CSS-Reihenfolge: `main.jsx` importiert `App` vor `global.css`, also lädt `serverpuls.css` vor `global.css`, und bei gleicher Spezifität gewinnt `global.css`. Jede Regel in `serverpuls.css`, die eine Global-Klasse überstimmt (`.stats`, `.list-row`, `.msg`, `.table`, `.seg`), hat höhere Spezifität (0,2,0, z. B. `.stats.sp-tiles`, `.sp-page .msg`) und nie `!important`.
|
||||
- Abstände zwischen aufeinanderfolgenden Abschnitten kommen von `.page section + section { margin-top: 36px }` aus P1. P9 baut dafür keine eigenen Regeln: `.sp-panel` und `.stats.sp-tiles` setzen keinen Außenabstand nach oben. Eigene Abstände gibt es nur innerhalb der Abschnitte (`div`-Container).
|
||||
- Die CSS-Tabellen dieses Plans nennen nur die Abweichungen vom Prototyp und von den Tokens. Was Basis (`*`-Reset, `button`-Reset), `.table`, `.stat`, `.list-row` und `.seg` schon liefern, wird nicht wiederholt.
|
||||
- Canvas-Farben stehen nur in `chartColors.js`. In `ServerpulsPage.jsx`, `Chart.jsx`, `AppsTable.jsx` und `Sparkline.jsx` gibt es kein Farbliteral.
|
||||
- Keine Icon-Importe, keine Emojis, keine Glyphen ▲ und ▼. Die Sortierzeichen ↑ und ↓ bleiben.
|
||||
- Sichtbare Textänderungen: keine außer den Badge-Texten des Kopfes aus `LEVEL_TEXT`. Alle Ladetexte („Lädt …“) und Meldungen bleiben wörtlich.
|
||||
- Keine neuen `style={{…}}`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
Aus der Übersicht gehören drei Punkte zu P9:
|
||||
- **Punkt 2, lange Inhalte am Handy:** Sechs Kacheln, Umschalter mit 6 und 5 Knöpfen und die Tabelle bei 390 px ohne waagerechtes Scrollen. Testdaten `serverpuls-lang`: Projektname mit 60 Zeichen ohne Leerzeichen, Warnungstitel mit 90 Zeichen. `overflow-x` ist seit dem Gate `p1` für alle Ansichten scharf, also auch im Gate `p9`.
|
||||
- **Punkt 3, Teilausfall:** Ansicht `serverpuls-ausfall`. Die Übersicht antwortet 500, `sp-error` erscheint, Diagramm und Tabelle bleiben nutzbar, die Kacheln zeigen „–“.
|
||||
- **Punkt 4, nur Tastatur:**
|
||||
- Projektzeilen lassen sich über den Button in der ersten Zelle öffnen (`aria-pressed`).
|
||||
- Sortierköpfe sind Buttons mit `aria-sort` auf dem `th`.
|
||||
- Die `.seg`-Knöpfe tragen `aria-pressed`.
|
||||
|
||||
Zusätzlich gilt für P9:
|
||||
- Checkliste der Bedienelemente, nichts darf fehlen: Metrik-Umschalter (6 Knöpfe), Zeitraum-Umschalter (5), Projektsuche, 6 Sortierköpfe (Projekt, Größe, Δ 7 T, Δ 30 T, CPU, RAM), Projektzeile (öffnet und schließt das Detail), Zeitraum-Umschalter im Detail (5).
|
||||
- Die 20 `data-testid` im Serverpuls-Code bleiben identisch (Task 1 und 6). Ohne den Muster-Eintrag `sp-tile-${id}` sind es die 19 der Spec.
|
||||
- Rot heißt gestiegen und Grün gesunken, das Vorzeichen steht weiter in der Zahl.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Paketstart und Ausgangswerte
|
||||
|
||||
**Files:** keine Änderung.
|
||||
|
||||
**Interfaces:** keine.
|
||||
|
||||
- [ ] **Step 1: Paketstart**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git merge-base --is-ancestor design-p8 HEAD && git rebase origin/test && "$W/check.sh" && git status --short`
|
||||
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0` und eine leere Statusliste. Endet `merge-base` mit Exit 1, liegt `design-p8` nicht im Branch: stoppen und melden. Die Testzahl heißt im Folgenden N0, die Bundle-Größen aus der Ausgabe von `check.sh` werden notiert.
|
||||
|
||||
- [ ] **Step 2: Abgleich mit TP2**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git log --oneline origin/test..feature/terminplaner-tp2 -- src/pages/ServerpulsPage.jsx src/components/serverpuls src/styles/serverpuls.css src/lib/serverpulsFormat.js src/styles/global.css`
|
||||
Expected: keine Ausgabe. Gibt es Treffer, stoppt der Agent vor Task 2, nennt dem Eigentümer die Commits und fragt nach. Er empfiehlt dabei, TP2 zuerst nach `test` zu bringen.
|
||||
|
||||
- [ ] **Step 3: Referenzliste der Test-IDs anlegen**
|
||||
|
||||
Run: ``source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -o --no-filename 'testid=("sp-[a-z-]+"|\{`sp-tile-\$\{id\}`\})' src/pages/ServerpulsPage.jsx src/components/serverpuls | sort > "$W/p9-testids.before.txt" && wc -l < "$W/p9-testids.before.txt"``
|
||||
Expected: `20`.
|
||||
|
||||
- [ ] **Step 4: Voraussetzungen aus P1 und P2 prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && ls src/components/serverpuls/chartColors.js src/components/ui/Stat.jsx src/components/ui/tones.js src/lib/rowClick.js && rg -n "isRowActivation" src/lib/rowClick.js && rg -n -m 1 "CHART\." src/components/serverpuls/Chart.jsx src/pages/ServerpulsPage.jsx && rg -n "#[0-9a-fA-F]{3,8}\b|rgba?\(" src/components/serverpuls src/pages/ServerpulsPage.jsx --glob '!chartColors.js'`
|
||||
Expected: Die Dateien sind da, `src/lib/rowClick.js` (P2 Task 2) enthält `isRowActivation(event)`, `CHART.` kommt in beiden Dateien vor, und die letzte Suche liefert keine Treffer (Exit 1). Fehlt `rowClick.js`, stoppt der Agent und meldet es, P9 baut keinen eigenen Zeilenklick-Helfer.
|
||||
|
||||
### Task 2: Format-Helfer `deltaDir`, `deltaSign`, `levelTone` (TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/serverpulsFormat.js` (neue Exporte nach `formatNumber`, vor dem Abschnitt `// --- Zeitachsen`)
|
||||
- Test: `tests/unit/serverpulsFormat.test.mjs` (Z. 5 Import, zwei neue Tests)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `deltaDir(d) → 'up' | 'down' | 'flat'` (`flat` bei Betrag unter `1e-9`, genau die Regel aus `Tile`).
|
||||
- `deltaSign(dir) → '+' | '−' | ''` (U+2212).
|
||||
- `levelTone(level) → 'ok' | 'warn' | 'danger' | 'muted'`. Es sind die Ton-Namen von `Badge`.
|
||||
- Bestehende Exporte bleiben unverändert.
|
||||
|
||||
- [ ] **Step 1: Failing Test schreiben**
|
||||
|
||||
Die Destrukturierung in Z. 5 bekommt `deltaDir, deltaSign, levelTone`. Dazu:
|
||||
|
||||
```js
|
||||
test('deltaDir und deltaSign: Richtung und Vorzeichen der Kachel-Deltas', () => {
|
||||
assert.deepEqual([deltaDir(0), deltaDir(5e-10), deltaDir(-5e-10)], ['flat', 'flat', 'flat'])
|
||||
assert.equal(deltaDir(0.4), 'up')
|
||||
assert.equal(deltaDir(-0.4), 'down')
|
||||
assert.deepEqual([deltaSign('up'), deltaSign('down'), deltaSign('flat')], ['+', '−', ''])
|
||||
})
|
||||
|
||||
test('levelTone: Badge-Ton je Stufe', () => {
|
||||
assert.deepEqual(['ok', 'medium', 'critical'].map(levelTone), ['ok', 'warn', 'danger'])
|
||||
assert.deepEqual([levelTone('low'), levelTone(undefined), levelTone('x')], ['muted', 'muted', 'muted'])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test laufen lassen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/serverpulsFormat.test.mjs`
|
||||
Expected: FAIL, `deltaDir is not a function`.
|
||||
|
||||
- [ ] **Step 3: Implementieren**
|
||||
|
||||
Die drei Funktionen in `src/lib/serverpulsFormat.js`. Die Schwelle `1e-9` und das Zeichen U+2212 sind wie in `Tile` und `formatDelta`.
|
||||
|
||||
- [ ] **Step 4: Test grün**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node --test tests/unit/serverpulsFormat.test.mjs && "$W/check.sh" --nur-tests`
|
||||
Expected: PASS, Exit 0, Testzahl N0 + 2, `# fail 0`.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/lib/serverpulsFormat.js tests/unit/serverpulsFormat.test.mjs
|
||||
git commit -m "Serverpuls: Richtung, Vorzeichen und Stufen-Ton als Format-Helfer mit Test" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 3: Werkzeug – Testdaten, Ansichten, Gate `p9`, Ausnahme für Sparklines
|
||||
|
||||
**Files** (alle unter `$W/`, kein Commit im Repo):
|
||||
- Create: `fixtures/serverpuls-ok.mjs`, `fixtures/serverpuls-veraltet.mjs`, `fixtures/serverpuls-lang.mjs`
|
||||
- Modify: `views.mjs`, `gates.mjs`, `checks.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `createBackend`, `VIEWS`, `GATES` aus P0 und die Konsolen-Ausnahme des Szenarios `fehler` (P0, in P8 Task 4 Step 3 erweitert).
|
||||
- Produces: die Ansichten aus Step 2, `GATES.p9`, die Ausnahme `svg.sp-spark` im Check `icon-svg` und die Ausnahme für `**/api/serverpuls/**` im Szenario `fehler`.
|
||||
|
||||
- [ ] **Step 1: Testdaten-Varianten anlegen**
|
||||
|
||||
Alle erweitern `standard`.
|
||||
- `serverpuls-ok`: Übersicht mit `level: 'ok'` und `alerts: []`.
|
||||
- `serverpuls-veraltet`: `level: 'medium'`, eine Warnung, `lastSample` vor 40 Minuten.
|
||||
- `serverpuls-lang`: `level: 'critical'` mit drei Warnungen (`critical`, `medium`, `low`), davon eine mit 90-Zeichen-Titel. Zwölf Projektzeilen, eine mit 60-Zeichen-Namen ohne Leerzeichen.
|
||||
|
||||
- [ ] **Step 2: Ansichten in `views.mjs` anlegen oder anpassen**
|
||||
|
||||
Der `path` ist `/serverpuls`. Die Schritte haben die Signatur `steps(page, ctx)`, `ctx.backend.calls` listet die vom Backend beantworteten Requests. Geprüft wird mit `throw new Error(…)`, das den Verstoß `step-failed` erzeugt.
|
||||
|
||||
| ID | Daten | Schritte |
|
||||
|---|---|---|
|
||||
| `serverpuls` | `serverpuls-lang` | Warten, bis `[data-testid="sp-chart"]` ein `canvas` mit Breite über 0 trägt und sechs `[data-testid^="sp-tile-"]` da sind. |
|
||||
| `serverpuls-ok` | `serverpuls-ok` | Wie `serverpuls`. |
|
||||
| `serverpuls-veraltet` | `serverpuls-veraltet` | Wie `serverpuls`. |
|
||||
| `serverpuls-projekt` | `serverpuls-lang` | Fokus auf `button.link` der ersten `[data-testid="sp-app-row"]`, Enter: `sp-detail` muss sichtbar sein und drei `canvas` enthalten. Fokus auf den Sortierkopf „Größe“ (`th[data-sort="sizeBytes"] button`), Enter: `aria-sort` muss `ascending` heißen. Klick auf „RAM“ in `[data-testid="sp-metric-select"]` (nicht auf den gleichnamigen Tabellenkopf), dann muss dieser Knopf `aria-pressed="true"` tragen. |
|
||||
| `serverpuls-ausfall` | `standard`, Szenario `fehler` | `GET **/api/serverpuls/overview` mit 500 `{ error: 'Test: Serverpuls ist nicht erreichbar.' }` beantworten. `sp-error` muss sichtbar sein, das Diagramm gezeichnet und die Tabelle da. |
|
||||
|
||||
- [ ] **Step 3: Gate festlegen**
|
||||
|
||||
In `gates.mjs` kommt nach der Deklaration von `GATES` dazu. Das Gate `p9` erbt Prüfungen und Ansichten von `p8` (die `p1`-Prüfungen für alle Ansichten, `step-failed`, `external-request` und `console-error` immer) und schaltet `icon-svg` zusätzlich für die Ansichten von P9 und aller früheren Pakete ein:
|
||||
|
||||
```js
|
||||
const iconSvg = GATES.p8.scoped['icon-svg']
|
||||
GATES.p9 = {
|
||||
...GATES.p8,
|
||||
scoped: {
|
||||
...GATES.p8.scoped,
|
||||
'icon-svg': {
|
||||
...iconSvg,
|
||||
views: [...iconSvg.views, 'serverpuls', 'serverpuls-ok', 'serverpuls-veraltet', 'serverpuls-projekt', 'serverpuls-ausfall'],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Sparklines vom Icon-Check ausnehmen**
|
||||
|
||||
`checks.mjs`: Der Check `icon-svg` nimmt `svg.sp-spark` dauerhaft aus (wie `.brand`), nicht über `ignore` im Gate. Es sind Datengrafiken (Inline-SVG-Minikurven), keine Icons. Die Konsolen-Ausnahme des Szenarios `fehler` deckt auch die Meldungen zu `**/api/serverpuls/**` ab, falls sie das noch nicht tut.
|
||||
|
||||
- [ ] **Step 5: Ausgangslauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9-vorher && "$W/shots.sh" p9-vorher "$W/shots/p9-vorher" --only serverpuls --gate p9`
|
||||
Expected: Exit 0. Der Stand nach P1 ist hell, und die Sparklines sind ausgenommen. Die Bilder dienen als Vergleich.
|
||||
|
||||
### Task 4: Kopf, Warnungen, Kacheln, Umschalter, Panels
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pages/ServerpulsPage.jsx` (`LEVEL_*` Z. 31–32, `Tile` Z. 79–100, `Pills` Z. 102–117, `Cleanup` Z. 119–139, Markup Z. 254–331; Abfragen und Hooks Z. 141–239 bleiben)
|
||||
- Rewrite: `src/styles/serverpuls.css` (ohne die Tabellenregeln, die bleiben bis Task 5)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Stat({ label, value, sub, tone })`, `PageHeader`, `badgeToneClass(tone)` aus `src/components/ui/tones.js`, `Badge`, `deltaDir`, `deltaSign`, `levelTone` (Task 2), `formatDelta`.
|
||||
- Produces: nichts für spätere Pakete.
|
||||
|
||||
- [ ] **Step 1: Kopf und Fehlerzeile**
|
||||
|
||||
- Fehler: `<div className="msg err" data-testid="sp-error" role="alert">`.
|
||||
- Kopf: `<section data-testid="sp-head">` ohne `sp-level-*`. Die Kopfzeile `div.sp-head-top` enthält:
|
||||
- `<span className={badgeToneClass(levelTone(level))} data-testid="sp-ampel" data-level={level}>{LEVEL_TEXT[level] || level}</span>`. `LEVEL_TEXT` ist die schon vorhandene, bisher ungenutzte Konstante („Alles ok“, „Warnungen“, „Critical“), das Emoji und `dot` entfallen.
|
||||
- die `<strong className="sp-headline">` mit dem unveränderten Text und `sp-age`. Bei `age.stale` kommt zusätzlich die Klasse `sp-age-stale`.
|
||||
- Ohne `overview` zeigt das Badge `badge plain` mit „–“, `data-level` bleibt `ok`.
|
||||
|
||||
Entscheidung: Das Badge nennt die Stufe aus `LEVEL_TEXT`, die Überschrift bleibt wörtlich („Alles ok“, „Critical – 2 Warnungen“, „2 Warnungen“). Die Doppelung ist gewollt. Ohne Daten steht „–“ statt „Alles ok“, damit der Fehlerzustand nicht grün aussieht.
|
||||
|
||||
- [ ] **Step 2: Warnungen**
|
||||
|
||||
`ul.sp-alerts[data-testid=sp-alerts]` mit `li.list-row.sp-alert[data-testid=sp-alert]`:
|
||||
- Links `div.sp-alert-main` mit `<Badge tone={levelTone(a.level)}>{LEVEL_LABEL[a.level] || a.level}</Badge>`, `span.sp-alert-title` und `span.sp-alert-value`.
|
||||
- Rechts `<span className="muted small nowrap">seit {formatSince(a.since)}</span>`.
|
||||
|
||||
- [ ] **Step 3: Kacheln, Umschalter, Panels, Aufräumbar**
|
||||
|
||||
- Kacheln: `<section className="stats sp-tiles" data-testid="sp-tiles">`. Jede Kachel ist `div.sp-tile[data-testid="sp-tile-<id>"]` mit `Stat label value sub`, `Sparkline width={120} height={32}` und der Delta-Zeile.
|
||||
- `sub` ist `model?.sub || '\u00a0'`, damit die Sparklines einer Reihe fluchten.
|
||||
- Die Delta-Zeile ist `div.sp-tile-delta.sp-delta-{dir}` mit `{deltaSign(dir)}{model.fmtDelta(d)} ggü. gestern`. `dir` kommt aus `deltaDir(d)`, die Glyphen ▲ und ▼ entfallen.
|
||||
- `Pills` rendert `<div className="seg" data-testid={testid}>` mit `<button type="button" className={active ? 'on' : undefined} aria-pressed={active}>`.
|
||||
- Panels: `<section className="sp-panel" data-testid="sp-chart-panel">` mit `<h2 className="section">Verlauf</h2>`, darunter `div.sp-controls` mit beiden `Pills`, dann das Diagramm.
|
||||
- `sp-projects` und `sp-cleanup` folgen demselben Muster.
|
||||
- „Größen von {Tag}“ steht als `<span className="muted small">` in `h2.section` rechts.
|
||||
- Die Detail-Überschrift ist `<h3 className="sp-detail-title">`, ihre `Pills` stehen in `div.sp-controls`.
|
||||
- „Aufräumbar“: `div.stats.sp-cleanup-stats` mit drei `Stat` („Ungenutzte Images“, „Build-Cache“, „Gestoppte Container“), die Liste der gestoppten Container als `li.list-row` mit Name und `<span className="num">`-Größe, der Stand-Satz als `<p className="muted small">`. „Noch keine Messung.“ bekommt `.muted`.
|
||||
- Alle `sp-muted`, `sp-small` werden `muted`, `small`. `.sp-nodata` bleibt als Klasse für „Lädt …“ und „Für diesen Zeitraum …“.
|
||||
|
||||
Entscheidung: Die Panels sind `<section data-testid>` mit `h2.section` statt `Card`, weil `Card` kein `data-testid` weitergibt. Die Umschalter stehen unter der Überschrift, nicht darin, damit der Kopf auf dem Handy nicht überläuft.
|
||||
|
||||
- [ ] **Step 4: `serverpuls.css` neu schreiben**
|
||||
|
||||
Gelöscht werden `.sp-page`-Flex, alle Kästen, Rahmen und Literale, `.sp-level-*`, `.sp-ampel`, `.sp-tile-label/-value/-sub`, `.sp-pill*`, `.sp-panel-head`, `.sp-cleanup-grid`. Neu:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.sp-page .msg` | `margin: 0 0 16px` |
|
||||
| `.sp-head-top` | `display: flex; align-items: center; gap: 12px; flex-wrap: wrap` |
|
||||
| `.sp-headline` | `font-size: 16px; line-height: 24px; font-weight: 600` |
|
||||
| `.sp-age` | `margin-left: auto; font-size: 13px; color: var(--text-2)` |
|
||||
| `.sp-age-stale` | `color: var(--orange-fg); font-weight: 500` |
|
||||
| `.sp-alerts` | `list-style: none; margin: 12px 0 0; padding: 0` |
|
||||
| `.sp-alert-main` | `display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; min-width: 0` |
|
||||
| `.sp-alert-title` | `font-weight: 500; overflow-wrap: anywhere` |
|
||||
| `.sp-alert-value` | `color: var(--text-2)` |
|
||||
| `.stats.sp-tiles` | `display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 24px 32px` (ohne `margin`) |
|
||||
| `.sp-tile` | `min-width: 0` |
|
||||
| `.sp-tile .sub` | `min-height: 16px` (Größe und Farbe kommen von `.stat .sub`) |
|
||||
| `.sp-spark` | `display: block; max-width: 100%; margin-top: 6px` |
|
||||
| `.sp-tile-delta` | `font-size: 12px; line-height: 16px; margin-top: 4px` |
|
||||
| `.sp-delta-up` / `-down` / `-flat` | `color: var(--red-fg)` / `var(--green-fg)` / `var(--text-2)` |
|
||||
| `.sp-panel` | `min-width: 0` (ohne `margin`) |
|
||||
| `.sp-controls` | `display: flex; flex-wrap: wrap; gap: 8px 16px; margin-bottom: 12px` |
|
||||
| `.sp-controls .seg` | `flex-wrap: wrap` |
|
||||
| `.sp-chart` | `width: 100%; min-height: 200px` |
|
||||
| `.sp-chart .u-legend` | `font-size: 13px; color: var(--text-2)` |
|
||||
| `.sp-nodata` | `padding: 40px 0; text-align: center; color: var(--text-2)` |
|
||||
| `.sp-detail` | `margin-top: 28px` (ein `div` im Panel, keine `section`) |
|
||||
| `.sp-detail-title` | `margin: 0 0 8px; font-size: 15px; line-height: 22px; font-weight: 600; overflow-wrap: anywhere` |
|
||||
| `.sp-detail-charts` | `display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); gap: 24px` |
|
||||
| `.sp-chart-title` | `font-size: 13px; color: var(--text-2); margin-bottom: 4px` |
|
||||
| `.sp-cleanup-list` | `list-style: none; margin: 12px 0; padding: 0` |
|
||||
| ≤ 1100 px `.stats.sp-tiles` | `grid-template-columns: repeat(3, minmax(0, 1fr))` |
|
||||
| ≤ 860 px `.stats.sp-tiles` | `grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px` |
|
||||
| ≤ 860 px `.sp-age` | `margin-left: 0` |
|
||||
| ≤ 860 px `.sp-page .sp-cleanup-stats` | `grid-template-columns: repeat(2, minmax(0, 1fr))` |
|
||||
|
||||
Den Abstand zwischen Kopf, Kacheln und Panels liefert `.page section + section` aus P1. `.stats.sp-tiles` und `.sp-panel` setzen deshalb keinen oberen Außenabstand. Ein `margin` auf `.stats.sp-tiles` würde den globalen Abstand wegen der höheren Spezifität überstimmen.
|
||||
|
||||
- [ ] **Step 5: Bauen und prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9 && "$W/shots.sh" p9 "$W/shots/p9-t4" --only serverpuls,serverpuls-ok,serverpuls-veraltet,serverpuls-ausfall --gate p9`
|
||||
Expected: PASS. In den Bildern gilt:
|
||||
- Keine Kästen um Kopf, Kacheln und Panels.
|
||||
- Das Badge trägt „Critical“ (rot), „Warnungen“ (orange) bzw. „Alles ok“ (grün). Veraltete Daten stehen als oranger Text.
|
||||
- Warnungen sind Zeilen mit Haarlinien und Stufen-Badge (Critical rot, Medium orange, Low grau).
|
||||
- Die Kacheln stehen bei 1440 px in sechs Spalten, bei 390 px in zwei. Deltas sind rot oder grün ohne Pfeil.
|
||||
- Der Umschalter ist grau mit weißem aktivem Teil und bricht bei 390 px um.
|
||||
- Das Diagramm hat eine schwarze Linie, die Spitze ist grau gestrichelt, es gibt nur horizontale Rasterlinien, und die Legende ist 13 px grau.
|
||||
- Kopf, Kacheln und Panels haben überall denselben Abstand (36 px aus `.page section + section`).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pages/ServerpulsPage.jsx src/styles/serverpuls.css
|
||||
git commit -m "Design: Serverpuls-Kopf mit Text-Badge, Warnungen als Zeilen, Kacheln als Kennzahlen" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 5: Projekttabelle
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/serverpuls/AppsTable.jsx` (Spaltenliste Z. 5–12, `Delta` Z. 14–18, Markup Z. 42–89; Sortier- und Filterlogik Z. 20–40 bleibt)
|
||||
- Modify: `src/styles/serverpuls.css` (Tabellenregeln `.sp-search` bis `.sp-delta` ersetzen)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `.table`, `tr.row`, `.toolbar`, `.input.search`, `.hide-sm`, `.num`, `.right` (P1), `isRowActivation(event)` aus `src/lib/rowClick.js` (P2 Task 2, Prüfung in Task 1), `formatBytes`, `formatDelta`, `formatPct`, `Sparkline`.
|
||||
- Produces: `AppsTable({ rows, selectedId, onSelect })` behält seine Props.
|
||||
|
||||
- [ ] **Step 1: Markup umbauen**
|
||||
|
||||
- Die Wurzel `div.sp-apps[data-testid=sp-apps]` bleibt. Die Suche steht in `<div className="toolbar">` als `<input className="input search" type="search" data-testid="sp-apps-search" placeholder="Projekt suchen …">`.
|
||||
- `<table className="table">` ohne `.sp-table-wrap`. Die Spaltenliste bekommt je Eintrag `num` (Größe, Δ 7 T, Δ 30 T, CPU, RAM) und `hide` (Δ 30 T, CPU und die Spalte „30 Tage“). Diese Spalten tragen `right num` bzw. `hide-sm` auf `th` und `td`.
|
||||
- Sortierkopf: `<th data-sort={c.key} aria-sort={…}>` mit `<button type="button" className="sp-sort" onClick={() => toggle(c.key)}>{c.label}<span aria-hidden="true">{' ↑' | ' ↓' | ''}</span></button>`.
|
||||
- `aria-sort` ist `ascending` bzw. `descending` am aktiven Kopf, sonst fehlt es.
|
||||
- Die Zeichen ↑ und ↓ bleiben wie bisher.
|
||||
- Zeile: ``<tr className={`row${selected ? ' sp-row-active' : ''}`} data-testid="sp-app-row" data-app-id={r.id}>``.
|
||||
- Der Zeilenklick ruft `onSelect(selectedId === r.id ? null : r.id)`, aber nur wenn `isRowActivation(e)` wahr ist.
|
||||
- Die erste Zelle ist `<td className="sp-name"><button type="button" className="link" aria-pressed={selected} onClick={() => onSelect(selectedId === r.id ? null : r.id)}>{r.name}</button></td>` mit demselben Aufruf. Der Button braucht ihn, weil `isRowActivation` Klicks auf `button` ausfiltert.
|
||||
- `Delta` rendert ``<span className={`sp-delta sp-delta-${d.dir}`}>{d.text}</span>`` ohne Pfeil. Das Vorzeichen steckt in `d.text`.
|
||||
- Die Leerzeile bleibt (`colSpan={7}`, „Keine Projekte gefunden.“, Klasse `sp-empty`).
|
||||
|
||||
Entscheidung: Die Zeilen folgen der Tabellenregel der Spec. Der Button in der ersten Zelle trägt die Tastaturbedienung, der Zeilenklick bleibt für die Maus. Unter 860 px bleiben Projekt, Größe, Δ 7 T und RAM.
|
||||
|
||||
- [ ] **Step 2: Tabellen-CSS ersetzen**
|
||||
|
||||
Gelöscht werden `.sp-search`, `.sp-table-wrap`, `.sp-table*`, `.sp-sortable`, `.sp-row`, `.sp-row:hover`, `.sp-delta`. Neu:
|
||||
|
||||
| Regel | Wert |
|
||||
|---|---|
|
||||
| `.sp-sort` | `display: block; width: 100%; text-align: inherit; white-space: nowrap` (Knopf-Reset und Innenabstand 0 kommen aus der Basis) |
|
||||
| `.sp-name` | `overflow-wrap: anywhere` |
|
||||
| `.sp-name .link` | `text-align: left; overflow-wrap: anywhere` |
|
||||
| `.sp-page .table tr.sp-row-active td` | `background: var(--bg-subtle)` |
|
||||
| `.sp-page .table tr.sp-row-active td:first-child` / `:last-child` | `box-shadow: -12px 0 0 var(--bg-subtle)` / `12px 0 0 var(--bg-subtle)` |
|
||||
| `.sp-empty` | `text-align: center; color: var(--text-2)` |
|
||||
| `.sp-delta` | `font-variant-numeric: tabular-nums` |
|
||||
|
||||
- [ ] **Step 3: Bauen und prüfen**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9 && "$W/shots.sh" p9 "$W/shots/p9-t5" --only serverpuls,serverpuls-projekt --gate p9`
|
||||
Expected: PASS, das heißt:
|
||||
- Die Tastaturschritte laufen durch.
|
||||
- Die Tabelle hat nur Haarlinien, Zahlen stehen rechts, die ausgewählte Zeile ist `--bg-subtle`.
|
||||
- Deltas sind rot, grün oder grau ohne Pfeil. Das Detail zeigt drei Diagramme in Schwarz.
|
||||
- Bei 390 px sind vier Spalten da und der lange Projektname bricht um.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/components/serverpuls/AppsTable.jsx src/styles/serverpuls.css
|
||||
git commit -m "Design: Serverpuls-Projekttabelle (Sortierkoepfe und Zeilen als Buttons, Deltas als Farbe)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
### Task 6: Gate P9, Review, Tag
|
||||
|
||||
- [ ] **Step 1: Rebase**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git fetch origin && git rebase origin/test && "$W/check.sh"`
|
||||
Expected: Exit 0, Rebase ohne Konflikt, `# fail 0`. Die Testzahl ist N0 + 2. Liegen neue Commits auf `test`, kann sie höher sein, die Zahl wird notiert. So zeigen die Bilder der folgenden Schritte den Stand, der getaggt wird.
|
||||
|
||||
- [ ] **Step 2: Vollständiger Gate-Lauf**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/build.sh" "$WT" p9 && "$W/shots.sh" p9 "$W/shots/p9" --gate p9`
|
||||
Expected: Exit 0. Das Gate `p9` prüft alle Ansichten mit den `p1`-Prüfungen (`overflow-x`, `dark-surface`, `low-contrast`) und `icon-svg` für die Ansichten von P2 bis P9. Die fünf P9-Ansichten haben Bilder bei 1440 und 390 px ohne Verstöße.
|
||||
|
||||
- [ ] **Step 3: Test-IDs, Farben, unveränderte Logik**
|
||||
|
||||
Run: ``source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -o --no-filename 'testid=("sp-[a-z-]+"|\{`sp-tile-\$\{id\}`\})' src/pages/ServerpulsPage.jsx src/components/serverpuls | sort | diff - "$W/p9-testids.before.txt"``
|
||||
Expected: keine Ausgabe, also dieselben 20 Einträge.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && rg -n "react-icons|@tabler|style=\{|#[0-9a-fA-F]{3,8}\b|rgba?\(|!important|▲|▼|🔴|🟠|🟢" src/pages/ServerpulsPage.jsx src/styles/serverpuls.css src/components/serverpuls --glob '!chartColors.js'`
|
||||
Expected: keine Treffer.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && git diff design-p8 -- src/pages/ServerpulsPage.jsx | rg '^[-+].*(setInterval|60000|visibilit|reqId|serverpulsApi)'; git diff --stat design-p8 -- src/components/serverpuls/Chart.jsx src/components/serverpuls/Sparkline.jsx`
|
||||
Expected: beide Ausgaben leer, weil Takt, Abfragen, `Chart.jsx` und `Sparkline.jsx` unverändert sind.
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && node "$W/css-klassen.mjs" "$WT" | node -e "const j=JSON.parse(require('fs').readFileSync(0));console.log(j.dead.filter(n=>/^sp-/.test(n)).concat(j.undefinedUsed.filter(n=>/^sp-/.test(n))).join('\n'))"`
|
||||
Expected: leer, also keine tote und keine undefinierte `sp-*`-Klasse.
|
||||
|
||||
- [ ] **Step 4: Bundle-Größe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" --nur-build`
|
||||
Expected: Exit 0, CSS kleiner als bei Task 1. Die Werte werden notiert.
|
||||
|
||||
- [ ] **Step 5: Code-Review (sonnet)**
|
||||
|
||||
Geprüft wird der Diff `design-p8..HEAD` gegen die Spec-Abschnitte „Diagrammfarben“, „Bausteine“ (Kennzahlen, Umschalter, Tabelle, Liste, Badge, Meldungen) und Paket P9. Wichtige Befunde werden behoben. Danach kommen Step 1 (Rebase) noch einmal und Step 2 als Kurzlauf mit `--only serverpuls,serverpuls-projekt`. Schwerpunkte:
|
||||
- Die Test-IDs und Abfragen sind unverändert.
|
||||
- Der Fehlerzustand zeigt kein grünes „Alles ok“.
|
||||
- Das Vorzeichen steht weiter in jeder Delta-Zahl.
|
||||
- Die Tastaturbedienung funktioniert (Zeilen, Sortierköpfe, Umschalter).
|
||||
|
||||
- [ ] **Step 6: Tag und Übergabe**
|
||||
|
||||
Run: `source /root/reports/ticketsystem-design-2026-10-04/werkzeug/env.sh && "$W/check.sh" && git tag design-p9 && git log --oneline origin/test..design-p9 && git rev-parse design-p9`
|
||||
|
||||
Der Eigentümer bekommt:
|
||||
- die Screenshots aus `$W/shots/p9` (Unterordner `1440x900` und `390x844`; Auswahl: `serverpuls`, `serverpuls-ok`, `serverpuls-projekt`, `serverpuls-ausfall`)
|
||||
- den Hinweis, dass das Kopf-Badge „Alles ok“ und die Überschrift „Alles ok“ doppelt stehen (Entscheidung aus Task 4), falls er die Doppelung streichen will
|
||||
- die Push-Zeile `git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p9^{commit}:refs/heads/test'`
|
||||
- die Rücknahme `git revert --no-commit design-p8..design-p9`, Commit und Push durch den Eigentümer (gilt nur, solange P9 das jüngste Paket auf `test` ist)
|
||||
|
||||
- [ ] **Step 7: Nach dem Push (erst, wenn der Eigentümer gepusht hat)**
|
||||
|
||||
- Im Log von `webklar-ticket-build` steht „Deployment completed successfully“.
|
||||
- `curl -s https://ticket.webklar.com/ | grep -o 'assets/index-[^"]*\.js'` entspricht `dist/index.html` des Tags.
|
||||
- Ein lesender Sichttest im Chrome des Eigentümers: Serverpuls mit Diagramm und Projekttabelle. Der Menüpunkt zeigt weiter den Punkt `sp-nav-dot`, wenn es Warnungen gibt.
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
Appwrite-Vorlage "MFA-Challenge" (Auth → Templates → Sprache Deutsch)
|
||||
Betreff: Dein Anmeldecode für WEBklar
|
||||
Platzhalter (Appwrite): {{otp}}, {{user}}
|
||||
Code gilt 1 Stunde (Appwrite fest).
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Dein Anmeldecode für WEBklar</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Dein Code: {{otp}} ͏ ͏ ͏</span>
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
|
||||
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Anmeldung</td>
|
||||
</tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#efe6fb;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#5a2a91;">Anmeldecode</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Hallo {{user}}, hier ist dein Code</td></tr>
|
||||
<tr><td class="px ink" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;color:#111111;">Gib ihn im Ticketsystem oder im Projekt-Admin ein, um die Anmeldung abzuschließen.</td></tr>
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td align="center" class="ink" style="padding:22px;font-family:'Courier New',Courier,monospace;font-size:36px;line-height:40px;font-weight:bold;letter-spacing:10px;color:#111111;">{{otp}}</td></tr></table></td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Der Code gilt 1 Stunde. Nicht du? Dann ändere bitte dein Passwort.</td></tr>
|
||||
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;"> </td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Automatische Nachricht von WEBklar · Bitte nicht antworten.</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,169 @@
|
||||
# Mitarbeiter-2FA per E-Mail-Code + Wechsel Ticketsystem ⇄ Projekt-Admin
|
||||
|
||||
Stand: 2026-09-30 · Status: Design vom Nutzer freigegeben (Teil 1 + Teil 2), Spec wartet auf Review
|
||||
|
||||
## Ziel
|
||||
|
||||
1. Jeder Mitarbeiter-Login (Ticketsystem ticket.webklar.com **und** Projekt-Admin project.webklar.com)
|
||||
verlangt nach dem Passwort einen 6-stelligen Code, der per E-Mail kommt. Pflicht für alle
|
||||
Mitarbeiter. Kunden bleiben unverändert.
|
||||
2. Aus der Seitenleiste des Ticketsystems springt man ohne zweiten Login ins Projekt-Admin; aus dem
|
||||
Projekt-Admin führt ein Button zurück ins Ticketsystem.
|
||||
|
||||
Entscheidungen des Nutzers: E-Mail-Code statt TOTP/KeePass; 2FA über Appwrite selbst (Variante A),
|
||||
nicht als reine Oberflächen-Sperre; Pflicht für alle Mitarbeiter.
|
||||
|
||||
## Ausgangslage (geprüft)
|
||||
|
||||
- Beide Systeme nutzen dieselben Appwrite-Konten (Appwrite **1.8.1**, Projekt `6a1058610003c5a13a05`).
|
||||
- Appwrite verschickt bereits Mails: `_APP_SMTP_HOST=mail.your-server.de:587`, Absender
|
||||
`WEBklar <noreply@webklar.com>`.
|
||||
- Appwrite-MFA (1.8.1): Ist `user.mfa = true` und die E-Mail bestätigt (`emailVerification = true`),
|
||||
braucht jede Sitzung 2 Faktoren. Eine Passwort-Sitzung hat nur den Faktor `password`; jeder
|
||||
Nicht-MFA-Aufruf liefert dann `401 user_more_factors_required`. Das gilt auch für **bereits
|
||||
bestehende** Sitzungen und für JWTs aus solchen Sitzungen.
|
||||
- `POST /v1/account/mfa/challenges` `{factor:'email'}` → `{ $id, expire }`, schickt die Mail
|
||||
(Vorlage `mfaChallenge`). Code gilt **1 Stunde**. Limit 10 Anfragen pro Nutzer und Zeitfenster.
|
||||
- `PUT /v1/account/mfa/challenges` `{challengeId, otp}` → Sitzung bekommt Faktor `email`.
|
||||
Falscher/abgelaufener Code: `401 user_invalid_token`.
|
||||
- Server-API mit Key: `PATCH /v1/users/:id/mfa {mfa}`, `PATCH /v1/users/:id/verification {emailVerification}`.
|
||||
- Ticketsystem: Appwrite-Web-SDK **13.0.2** (kennt kein MFA). AuthContext meldet per
|
||||
`createEmailPasswordSession` an und prüft mit `account.get()`.
|
||||
- Projekt-Admin: `POST /api/auth/login` ruft serverseitig `/account/sessions/email` ohne API-Key auf,
|
||||
nimmt nur die `userId` und setzt die Portal-Sitzung (`role: 'admin'`) für Mitarbeiter
|
||||
(`resolveStaffAccount`). **Mit MFA wäre das eine Umgehung**, weil die halbe Sitzung reicht.
|
||||
- `requireAppwriteStaff` (JWT-Prüfung für `/api/admin/*` aus dem Ticketsystem) verlangt das
|
||||
Label `admin`. Mitarbeiter ohne Admin-Label kommen per Portal-Login trotzdem ins Projekt-Admin
|
||||
(`staff.isAdmin = false`).
|
||||
- Einmal-Link-Mechanismus existiert: `services/staffPreviewTokens.js` (Map im Speicher, 60 s, einmalig),
|
||||
genutzt von `/api/admin/preview-open` → `/api/auth/staff-preview`.
|
||||
- Projekt-Admin (`public/admin.html`) hat keine Seitenleiste, sondern Kopfzeile + Tabs.
|
||||
|
||||
## Teil 1: Anmeldung mit E-Mail-Code
|
||||
|
||||
### Ablauf für Mitarbeiter
|
||||
|
||||
1. E-Mail + Passwort.
|
||||
2. Anzeige „Wir haben dir einen Code an k…@webklar.com geschickt“ + 6-stelliges Codefeld
|
||||
(`inputmode="numeric"`, `autocomplete="one-time-code"`), Link „Code erneut senden“, Link „Abbrechen“
|
||||
(meldet die halbe Sitzung ab, zurück zur Passwort-Eingabe).
|
||||
3. Richtiger Code → angemeldet. Falscher/abgelaufener Code → „Code falsch oder abgelaufen“, Feld bleibt.
|
||||
|
||||
### Ticketsystem (Repo JUSN/tickte-system)
|
||||
|
||||
- **Kein SDK-Upgrade.** Neues Modul `src/lib/mfa.js` mit zwei Funktionen über `client.call(...)` des
|
||||
vorhandenen SDK-Clients (nutzt dieselbe Sitzung/Cookie-Fallback wie das SDK):
|
||||
`createEmailChallenge()` → `challengeId`; `completeChallenge(challengeId, otp)`.
|
||||
Beide senden `X-Appwrite-Locale: de`, damit die deutsche Vorlage greift.
|
||||
- `AuthContext`:
|
||||
- neuer Zustand `mfaPending` (`{ challengeId, email }` oder `null`).
|
||||
- `login()`: nach der Passwort-Sitzung `account.get()`; bei `user_more_factors_required` →
|
||||
Challenge anlegen, `mfaPending` setzen, `{ success: false, mfaRequired: true }` zurück.
|
||||
- `checkUser()` beim Laden: gleicher Fall (bestehende halbe Sitzung, z. B. direkt nach dem
|
||||
Einschalten) → **keine** neue Mail automatisch, sondern Code-Schritt mit Knopf „Code senden“.
|
||||
Grund: jedes Neuladen würde sonst eine Mail auslösen und das Limit (10) aufbrauchen.
|
||||
- `verifyCode(otp)`, `resendCode()`, `cancelMfa()` (= `deleteSession('current')`).
|
||||
- Nach erfolgreichem Code wie bisher `account.get()` + `ensureEmployeeExists`.
|
||||
- `LoginPage.jsx`: zweiter Schritt „Code eingeben“; Routing: solange `mfaPending`, führt jede
|
||||
geschützte Route auf `/login`.
|
||||
- Fehlertexte: `user_invalid_token` → „Code falsch oder abgelaufen“; `429`/`general_rate_limit_exceeded`
|
||||
→ „Zu viele Versuche, bitte einige Minuten warten“; `user_email_not_verified` →
|
||||
„2FA ist für dein Konto noch nicht eingerichtet – bitte Kenso Bescheid geben“.
|
||||
|
||||
### Projekt-Admin (Repo Kundenportal, project.webklar.com)
|
||||
|
||||
- `loginWithAppwrite` liest zusätzlich die Sitzung aus der Antwort (`set-cookie`
|
||||
`a_session_<projectId>` via `headers.getSetCookie()`), damit der Server im Namen dieser Sitzung
|
||||
weiterarbeiten kann.
|
||||
- `POST /api/auth/login` für **Mitarbeiter**: Ist `account.mfa === true` (aus `getUserById`), wird
|
||||
**keine** Portal-Sitzung gesetzt. Stattdessen:
|
||||
- Challenge über die Sitzung anlegen, Eintrag in einer Map im Speicher
|
||||
`{ userId, email, appwriteSessionCookie, challengeId, expiresAt: +10 min, attempts: 0 }`
|
||||
unter zufälligem Schlüssel; Schlüssel als signiertes, httpOnly-Cookie `webklar_portal_mfa`.
|
||||
- Antwort `{ mfaRequired: true, email: <maskiert> }`.
|
||||
- `POST /api/auth/login/mfa` `{ otp }`: prüft den Code bei Appwrite (`PUT /account/mfa/challenges`
|
||||
mit der gespeicherten Sitzung). Erfolg → exakt der bisherige Mitarbeiter-Zweig (Portal-Sitzung +
|
||||
Preview-Cookie), Map-Eintrag + Cookie löschen. Max. 5 Fehlversuche pro Eintrag, dann Eintrag weg
|
||||
→ neu anmelden.
|
||||
- `POST /api/auth/login/mfa/resend`: neue Challenge für denselben Eintrag.
|
||||
- Serverneustart verwirft offene Einträge → Nutzer meldet sich neu an (akzeptiert).
|
||||
- Kunden-Zweig bleibt unverändert (Kunden haben `mfa = false`).
|
||||
- `public/login.html` + `app.js`: zweiter Schritt mit Codefeld, gleiche Texte wie im Ticketsystem.
|
||||
|
||||
### Einmalige Einrichtung (Skript, nach dem Livegang beider Teile)
|
||||
|
||||
- Skript `scripts/mitarbeiter-2fa.mjs` im Kundenportal-Repo (läuft im Container mit dem Server-Key):
|
||||
- `--status`: listet alle Mitarbeiter (Collection `employees` → `userId`) mit `emailVerification`
|
||||
und `mfa`.
|
||||
- `--enable [userId|alle]`: setzt `emailVerification = true`, dann `mfa = true`.
|
||||
- `--disable <userId>`: Notfall-Schalter, wenn bei einer Person keine Mail ankommt.
|
||||
- Vor dem Einschalten wird die E-Mail-Adresse jedes Kontos angezeigt; nur Adressen, die
|
||||
tatsächlich Post empfangen, werden bestätigt.
|
||||
- Wirkung: Alle bestehenden Mitarbeiter-Sitzungen fragen beim nächsten Aufruf nach dem Code.
|
||||
- Keine Wiederherstellungscodes (Faktor ist das Postfach; Notfall = `--disable`).
|
||||
|
||||
### Code-Mail
|
||||
|
||||
- Appwrite-Vorlage `mfaChallenge`, Sprache `de`, als eigene Vorlage im Projekt hinterlegt
|
||||
(Betreff „Dein Anmeldecode für WEBklar“, Inhalt im Stil der WEBklar-Mailvorlagen: Logo, Code groß,
|
||||
„gilt 1 Stunde“, „Nicht du? Dann ändere bitte dein Passwort.“).
|
||||
- Wird über die Appwrite-Console gesetzt (Auth → Templates) – der Nutzer ist dort eingeloggt; ich
|
||||
bereite HTML und Betreff vor.
|
||||
|
||||
## Teil 2: Wechsel zwischen Ticketsystem und Projekt-Admin
|
||||
|
||||
### Hinweg (Ticketsystem → Projekt-Admin, ohne zweiten Login)
|
||||
|
||||
- `Navbar.jsx`: neuer Eintrag **„Projekt-Admin“** mit Symbol `IconExternalLink` nach „Finanzen“ /
|
||||
„Admin“, für alle Mitarbeiter sichtbar. Klick → `openProjectAdmin()` → `window.location.assign(openUrl)`
|
||||
(selber Tab). Während des Ladens deaktiviert; bei Fehler Fallback auf
|
||||
`https://project.webklar.com/login.html`.
|
||||
- Kundenportal: `POST /api/admin/switch-link` (Bearer-JWT aus dem Ticketsystem):
|
||||
- eigene Prüfung statt `requireAppwriteStaff`: JWT gültig (`/account`) **und** `resolveStaffAccount`
|
||||
findet einen Mitarbeiter – so kommen auch Mitarbeiter ohne Admin-Label rüber, wie beim normalen
|
||||
Login.
|
||||
- Antwort `{ openUrl: 'https://project.webklar.com/api/auth/staff-switch?token=…' }`, Token über
|
||||
`createStaffPreviewToken` (60 s, einmalig) mit `{ userId, email, name, isAdmin, purpose: 'switch' }`.
|
||||
- `GET /api/auth/staff-switch?token=…`: `consumeStaffPreviewToken`; nur `purpose === 'switch'`
|
||||
akzeptiert (und `staff-preview` akzeptiert umgekehrt keine Switch-Tokens). Setzt Portal-Sitzung +
|
||||
Preview-Cookie wie der Mitarbeiter-Login, Redirect `/admin.html`. Ungültig/abgelaufen → Redirect
|
||||
`/login.html?error=Link abgelaufen, bitte anmelden`.
|
||||
- 2FA bleibt gewahrt: Das JWT stammt aus einer Sitzung, die nach Teil 1 nur mit Code voll gültig ist.
|
||||
|
||||
### Rückweg (Projekt-Admin → Ticketsystem)
|
||||
|
||||
- `admin.html` Kopfzeile: Button **„← Ticketsystem“** (Link `https://ticket.webklar.com/tickets`)
|
||||
vor „Abmelden“. Nur in der Admin-Ansicht; im Modus „Als Kunde ansehen“ und bei Kunden nicht.
|
||||
- Kein automatischer Login in diese Richtung: Ist die Ticketsystem-Sitzung noch aktiv, ist man direkt
|
||||
drin, sonst Login mit Code.
|
||||
|
||||
## Tests
|
||||
|
||||
Mit nachgebautem Appwrite (Mock nach 1.8.1-Verhalten, keine echten Daten):
|
||||
|
||||
- Ticketsystem: Login ohne MFA unverändert; Login mit MFA → Code-Schritt; richtiger Code;
|
||||
falscher Code; abgelaufener Code; erneut senden; Abbrechen; halbe Sitzung beim Neuladen (keine
|
||||
automatische Mail); 429-Text; Seitenleisten-Eintrag öffnet `openUrl`, Fehler → Fallback.
|
||||
- Projekt-Admin: Kunden-Login unverändert; Mitarbeiter ohne MFA unverändert; Mitarbeiter mit MFA
|
||||
bekommt **keine** Portal-Sitzung vor dem Code; richtiger/falscher Code; 5 Fehlversuche; resend;
|
||||
switch-link ohne/mit ungültigem JWT (401), mit halber Sitzung (401), Kunde (403), Mitarbeiter ohne
|
||||
Admin-Label (200); staff-switch einmalig, abgelaufen, falscher `purpose`; Rückweg-Button nur für
|
||||
Mitarbeiter und nicht im „Als Kunde ansehen“-Modus.
|
||||
- Bestehende Testsuites beider Repos grün.
|
||||
|
||||
## Livegang (Reihenfolge)
|
||||
|
||||
1. Kundenportal live (Push nach `main` durch den Nutzer bzw. auf sein OK).
|
||||
2. Ticketsystem live (Push nach `test` durch den Nutzer).
|
||||
3. Code-Mail-Vorlage in der Appwrite-Console hinterlegen.
|
||||
4. `mitarbeiter-2fa.mjs --status`, dann zuerst nur für Kenso `--enable`, einmal in beiden Systemen
|
||||
durchspielen, dann `--enable alle`.
|
||||
|
||||
Ohne Schritt 4 ändert sich für niemanden etwas (MFA aus → Login wie bisher); Teil 2 funktioniert
|
||||
schon nach Schritt 1+2.
|
||||
|
||||
## Nicht enthalten
|
||||
|
||||
- TOTP/KeePass, SMS, Wiederherstellungscodes, 2FA für Kunden.
|
||||
- Automatischer Login vom Projekt-Admin zurück ins Ticketsystem.
|
||||
- SDK-Upgrade des Ticketsystems.
|
||||
@@ -0,0 +1,72 @@
|
||||
# WOMS „Rechtliches“ – Konzept
|
||||
|
||||
Stand 2026-09-30, freigegeben im Gespräch (Umfang A, Rolle „Archiv + Überblick“, Anbieter mit Fristen).
|
||||
|
||||
## Ziel
|
||||
|
||||
Ein interner Menüpunkt **Rechtliches** (`/rechtliches`) in WOMS, der alle Rechtstexte von WEBklar mit
|
||||
lückenloser Versionsgeschichte und alle Verträge mit Anbietern samt Fristen an einer Stelle zeigt.
|
||||
Nur Mitarbeiter, keine Kunden. Die Texte werden weiter auf webklar.com und im Kundenportal gepflegt;
|
||||
WOMS speichert feste Kopien jeder Version (Archiv, keine Quelle).
|
||||
|
||||
Nicht im Umfang: Firmenunterlagen, kundenbezogene Dokumente, Mail-Erinnerungen, automatische
|
||||
Übernahme neuer Versionen aus den Deploys.
|
||||
|
||||
## Oberfläche
|
||||
|
||||
Seite mit zwei Reitern.
|
||||
|
||||
**Unsere Rechtstexte** – je Text eine Karte: AGB, Abo-Bedingungen, AVV, Datenschutz Website,
|
||||
Datenschutz Portal, Impressum, Widerrufsbelehrung/-formular. Karte zeigt aktuelle Version, „gilt seit“,
|
||||
Link zur Live-Seite. Klick öffnet die Versionsgeschichte (neu → alt): Nummer, gültig von/bis,
|
||||
Änderungsnotiz, Datei ansehen/herunterladen. Aktuell = grün, Entwurf = graues Abzeichen.
|
||||
„Neue Version“: Datei, Nummer, gültig ab, Notiz, Status. Wird eine Version gültig, bekommt die bisher
|
||||
gültige automatisch `validTo` = neues `validFrom` minus 1 Tag und Status „abgelöst“.
|
||||
|
||||
**Verträge mit Anbietern** – Liste mit Anbieter, Zweck, Kosten, Laufzeit, Kündigungsfrist, nächster
|
||||
Kündigungstermin; sortiert nach nächstem Termin. Rot ≤ 30 Tage, orange ≤ 90 Tage, grau = gekündigt/beendet,
|
||||
„Fristen fehlen“ wenn Angaben unvollständig. Detailansicht mit Dateien und Notiz; Anlegen, Bearbeiten,
|
||||
„Als gekündigt markieren“.
|
||||
|
||||
## Daten (Appwrite `woms-database`)
|
||||
|
||||
- `legalDocuments`: `key` (z. B. `agb`), `title`, `liveUrl`, `sort`.
|
||||
- `legalVersions`: `documentKey`, `version` (Text, z. B. „v6“), `validFrom`, `validTo`,
|
||||
`status` (`draft` | `current` | `superseded`), `changeNote`, `fileIds[]`.
|
||||
- `providerContracts`: `provider`, `purpose`, `cost`, `costInterval` (`month` | `year`), `startDate`,
|
||||
`termMonths`, `renewalMonths` (0 = keine Verlängerung), `noticeValue` + `noticeUnit` (`days` | `months`),
|
||||
`cancelledAt`, `endsAt`, `note`, `fileIds[]`.
|
||||
|
||||
Bucket `legal-files`: PDF, HTML, Bilder, DOC/DOCX, max. 20 MB.
|
||||
Rechte: `users` dürfen lesen, anlegen, bearbeiten. Kein Löschen bei Versionen und Dateien (Archiv bleibt
|
||||
vollständig); Anbieter-Verträge dürfen gelöscht werden.
|
||||
Anlage per Setup-Skript (`scripts/setup-legal.mjs`, idempotent wie `setup-woms-appwrite.mjs`),
|
||||
braucht einmal einen Appwrite-API-Key.
|
||||
|
||||
## Code
|
||||
|
||||
- `src/lib/legal.js`: `nextCancellationDate(contract, today)`, `deadlineTone(date, today)`,
|
||||
`supersedePrevious(versions, newVersion)`; reine Funktionen.
|
||||
- `src/hooks/useLegal.js`: Laden/Speichern der drei Sammlungen, Datei-Upload/-Link über Appwrite Storage.
|
||||
- `src/pages/LegalPage.jsx` mit den Reitern; Route und Menüpunkt in `App.jsx`/`Navbar.jsx`.
|
||||
|
||||
`nextCancellationDate`: Ende der laufenden Periode = Start + Laufzeit, danach je + Verlängerung, bis das
|
||||
Periodenende nach heute liegt; Termin = Periodenende minus Kündigungsfrist (Monatsende korrekt geklemmt,
|
||||
z. B. 31.01. + 1 Monat = 28./29.02.). Liegt der Termin schon vorbei, zählt die nächste Periode.
|
||||
Ohne Verlängerung: Vertragsende, danach „beendet“. Gekündigt: kein Termin, grau. Fehlende Angaben: `null`
|
||||
→ „Fristen fehlen“.
|
||||
|
||||
## Erstbefüllung
|
||||
|
||||
Skript `scripts/seed-legal.mjs`: Versionen aus der Git-Geschichte von webklar.com (`JUSN/Webklar`) und
|
||||
dem Kundenportal rekonstruieren (je live gegangene Fassung eine Version, Datum = Commit auf main,
|
||||
gespeichert als HTML/PDF). Die Versionsliste bekommt der Nutzer **vor dem Eintragen** zur Prüfung,
|
||||
damit die Nummerierung (AGB aktuell „v6“) zu seiner passt. Nicht-live-Stände (z. B. Abo-Bedingungen v4)
|
||||
als `draft`. Anbieter Hetzner (Server, Mail), Porkbun, Stripe als Einträge ohne geratene Fristen/Preise.
|
||||
|
||||
## Test und Auslieferung
|
||||
|
||||
Unit-Tests für `lib/legal.js` (Monatsende, Schaltjahr, keine Verlängerung, gekündigt, fehlende Angaben,
|
||||
Ablösung). Playwright-Test gegen Vite-Build mit gemocktem Appwrite (Reiter, Karte, Versionsgeschichte,
|
||||
neue Version löst alte ab, Farben der Fristen). Worktree `dev-rechtliches`, Branch `feature/rechtliches`;
|
||||
live erst nach OK des Nutzers per `push … :test`.
|
||||
@@ -0,0 +1,198 @@
|
||||
# Serverpuls – Server-Monitoring in WOMS
|
||||
|
||||
Stand: 2026-09-30 · Status: Design vom Nutzer im Chat freigegeben, Spec zur Prüfung
|
||||
|
||||
## Ziel
|
||||
|
||||
Ein neuer Navbar-Eintrag **Serverpuls** in WOMS (ticket.webklar.com) zeigt Admins die
|
||||
Auslastung des Servers und das Wachstum der einzelnen Projekte über Tage, Wochen und
|
||||
Monate – als Verlaufskurven „wie bei Aktien“. Feste Warnregeln (keine KI) melden
|
||||
Probleme per E-Mail in drei Stufen.
|
||||
|
||||
Ausdrücklich **nicht KI-gesteuert**: Messung, Auswertung und Mails laufen
|
||||
deterministisch in einem kleinen Dienst ohne Claude.
|
||||
|
||||
## Rahmenbedingungen
|
||||
|
||||
- Server: 8 Kerne, 15 GB RAM, 4 GB Swap (am 2026-09-30 voll), Platte 150 GB (76 % belegt),
|
||||
~61 Container. Der Dienst selbst muss sehr sparsam sein (Limit 128 MB RAM, 0,5 CPU).
|
||||
- WOMS ist eine statische React/Vite-SPA auf Appwrite; Server-Logik liegt in kleinen
|
||||
Node-Diensten hinter dem WOMS-nginx (Vorbild: `ticket-integrations`,
|
||||
`/api/integrations/`, Auth über Appwrite-JWT).
|
||||
- Admin in WOMS = Appwrite-Konto mit Label `admin` (`AuthContext.jsx`).
|
||||
- Der Deploy-Webhook von WOMS deployt jeden gepushten Branch; live geht nur per Push
|
||||
des Nutzers nach `test`.
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Frage | Entscheidung |
|
||||
|---|---|
|
||||
| Name | Serverpuls |
|
||||
| Rolle der KI | keine; feste Warnregeln |
|
||||
| „Projekt“ | jede App / jeder Container (Previews als eigene Apps) mit CPU, RAM, Speicher |
|
||||
| Warnkanal | E-Mail in drei Stufen Low / Medium / Critical |
|
||||
| Mail-Takt | Critical sofort, Medium Sammelmail täglich 07:00, Low Wochenübersicht Mo 07:00 |
|
||||
| Mail-Adresse | `server@webklar.com` ist Absender **und** einziger Empfänger |
|
||||
| Umsetzung | eigener schlanker Dienst (kein Beszel, kein Netdata) |
|
||||
| Sichtbarkeit | nur Admins (Navbar und API) |
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
Host /proc (ro) ──┐
|
||||
docker-socket-proxy (nur GET containers/stats) ──┼──> serverpuls (Node 22, SQLite) ──> SMTP server@
|
||||
/home/webklar/apps + Docker-Volumes (ro) ──┘ │
|
||||
└── HTTP :8091 <── WOMS-nginx /api/serverpuls/ <── ServerpulsPage (uPlot)
|
||||
```
|
||||
|
||||
### Einheiten
|
||||
|
||||
1. **Collector** (`collect/`): liest Messwerte, gibt reine Datenobjekte zurück.
|
||||
- `host.js` – `/proc/stat` (CPU % aus Differenz), `/proc/meminfo` (RAM verfügbar,
|
||||
Swap), `/proc/loadavg`, Zombies = Prozesse mit State `Z` in `/proc/*/stat`.
|
||||
Plattenbelegung `/` per `statfs` auf `/host/apps` (gleiches Dateisystem `/dev/sda1`).
|
||||
- `docker.js` – über den Socket-Proxy: Container-Liste (Name, Compose-Projekt/-Service,
|
||||
Status, Health, RestartCount) und `stats?stream=false` (CPU %, RAM).
|
||||
- `sizes.js` – Größe pro App: Ordner unter `/home/webklar/apps/<app>` plus Docker-
|
||||
Volumes der Container dieser App (`du -sb`, unter `nice -n 19 ionice -c3`).
|
||||
Außerdem „Aufräumbar“: ungenutzte Images, Build-Cache, gestoppte Container
|
||||
(`/system/df` über den Proxy).
|
||||
2. **App-Zuordnung** (`apps.js`): Container → App über das Label
|
||||
`com.docker.compose.service` bzw. den Containernamen; eine kleine Mapping-Datei
|
||||
`config/apps.json` erlaubt Umbenennen/Zusammenfassen (z. B. `webklar-ticket-*` →
|
||||
„Ticketsystem“) und Ordnerzuordnung. Previews erscheinen einzeln.
|
||||
3. **Speicher** (`store.js`): SQLite (`node:sqlite`) im Volume `serverpuls-data`.
|
||||
- Tabellen: `host_samples`, `app_samples`, `app_sizes`, `cleanup_samples`,
|
||||
`alerts` (Zustand), `mail_queue`.
|
||||
- Aufbewahrung: Rohwerte (5 min) 7 Tage → Stundenmittel/-max 90 Tage →
|
||||
Tageswerte unbegrenzt. Verdichtung täglich 03:30.
|
||||
- Erwartete Größe < 100 MB/Jahr.
|
||||
4. **Regeln** (`rules.js`): reine Funktion `(aktuelle + historische Werte) → Liste aktiver
|
||||
Befunde {id, level, titel, wert, seit}`. Schwellen in `config/rules.json`.
|
||||
5. **Alarmierung** (`alerts.js`): vergleicht Befunde mit gespeichertem Zustand, erzeugt
|
||||
Mails (sofort/täglich/wöchentlich), Wiederholungssperre 24 h, „✅ behoben“-Mail beim
|
||||
Verschwinden. Zustand in SQLite → Neustart erzeugt keine Doppel-Mails.
|
||||
6. **Mailer** (`mail.js`): nodemailer, `mail.your-server.de:587` STARTTLS, Login
|
||||
`server@webklar.com`. Fehlgeschlagene Mails bleiben in `mail_queue` und werden
|
||||
alle 5 min erneut versucht (max. 24 h).
|
||||
7. **API** (`api.js`): HTTP-Server Port 8091, Basis `/api/serverpuls`. Jede Anfrage
|
||||
(außer `/health`) braucht `X-Appwrite-JWT`; der Dienst fragt `/account` bei Appwrite
|
||||
ab (Cache 60 s) und verlangt Label `admin`, sonst 401/403.
|
||||
8. **Scheduler** (`main.js`): 5-min-Takt, nächtliche Größenmessung 03:00, Verdichtung
|
||||
03:30, Medium-Digest täglich 07:00, Low-Wochenübersicht Mo 07:00 (Europe/Berlin).
|
||||
|
||||
### Messung
|
||||
|
||||
- Alle 5 Minuten: Server gesamt (CPU %, Load 1/5/15, RAM verfügbar, Swap belegt,
|
||||
Platte `/`, Zombies) und pro Container (CPU %, RAM, Status, Neustarts).
|
||||
- Täglich 03:00: Größe pro App und „Aufräumbar“-Werte.
|
||||
- Fällt eine Messung aus, wird **kein** Wert geschrieben (Lücke statt falscher Null).
|
||||
|
||||
## Warnregeln
|
||||
|
||||
Wichtige Container („kritisch“): Traefik, Appwrite, Kundenportal, Ticketsystem,
|
||||
ticket-integrations, Gitea, InvoiceNinja, n8n (Liste in `config/rules.json`).
|
||||
|
||||
**🔴 Critical – sofortige Mail**
|
||||
- Platte `/` ≥ 90 %
|
||||
- RAM verfügbar < 500 MB **und** Swap ≥ 90 %, 10 min lang
|
||||
- Load (5 min) > 24, 15 min lang
|
||||
- kritischer Container läuft nicht oder > 3 Neustarts pro Stunde
|
||||
- Zombies ≥ 100
|
||||
|
||||
**🟠 Medium – Sammelmail täglich 07:00**
|
||||
- Platte `/` ≥ 80 %
|
||||
- Swap ≥ 75 % oder RAM verfügbar < 1,5 GB, jeweils 1 h lang
|
||||
- Zombies ≥ 20
|
||||
- App wächst in 7 Tagen um > 20 % oder > 2 GB
|
||||
- Load (5 min) > 8, 1 h lang
|
||||
- nicht-kritischer Container abgestürzt (exited ≠ 0 oder Restart-Schleife)
|
||||
|
||||
**🟡 Low – Wochenübersicht Mo 07:00**
|
||||
- ungenutzte Docker-Images > 10 GB oder Build-Cache > 3 GB
|
||||
- App wächst in der Woche um > 10 %
|
||||
- Zombies (≥ 1) halten sich > 24 h
|
||||
- immer enthalten: Top 5 größte Apps, Top 5 schnellstes Wachstum, Top 5 RAM
|
||||
|
||||
**Allgemein**
|
||||
- Dieselbe Warnung (gleiche `id`) höchstens 1× pro 24 h erneut gemeldet.
|
||||
- Beim Verschwinden: Mail „✅ behoben“ (in derselben Taktung wie die Stufe; bei
|
||||
Critical sofort).
|
||||
- Alle aktiven Warnungen stehen jederzeit auf der Seite.
|
||||
- Wird eine Warnung höher eingestuft (Medium → Critical), gilt sofort die höhere Stufe.
|
||||
|
||||
## API
|
||||
|
||||
Alle Antworten JSON, Zeitstempel ISO/UTC.
|
||||
|
||||
| Methode + Pfad | Inhalt |
|
||||
|---|---|
|
||||
| `GET /health` | ohne Auth; `{ok, lastSample}` |
|
||||
| `GET /overview` | aktuelle Serverwerte, 24-h-Minikurven, Trend ggü. gestern, aktive Warnungen, Zeitpunkt der letzten Messung |
|
||||
| `GET /host?metric=cpu\|ram\|swap\|load\|disk\|zombies&range=24h\|7d\|30d\|90d\|1y` | Zeitreihe; Auflösung abhängig vom Zeitraum |
|
||||
| `GET /apps` | Tabelle: Name, CPU %, RAM, Speicher, Δ 7 T, Δ 30 T, Speicher-Minikurve (30 T) |
|
||||
| `GET /apps/:id?range=…` | Zeitreihen Speicher, RAM, CPU für eine App |
|
||||
| `GET /cleanup` | ungenutzte Images, Build-Cache, gestoppte Container mit Größe |
|
||||
|
||||
Nur lesend. Es gibt **keine** Endpunkte zum Löschen, Stoppen oder Ändern.
|
||||
|
||||
## WOMS-Seite „Serverpuls“
|
||||
|
||||
- Navbar-Eintrag mit Pulssymbol, nur für `isAdmin`; roter/oranger Punkt bei aktiver
|
||||
Critical/Medium-Warnung (aus `/overview`, beim Laden der App und alle 5 min).
|
||||
- Route `/serverpuls`; Nicht-Admins werden umgeleitet.
|
||||
- Aufbau:
|
||||
1. **Kopf mit Ampel**: 🟢 Alles ok / 🟠 n Warnungen / 🔴 Critical, darunter die
|
||||
aktiven Warnungen (Stufe, seit wann, Wert). Hinweis „letzte Messung vor X Min.“;
|
||||
gelb, wenn älter als 15 min.
|
||||
2. **Kacheln**: CPU, Load, RAM, Swap, Platte, Zombies – aktueller Wert, 24-h-Minikurve,
|
||||
Trend ggü. gestern.
|
||||
3. **Großes Diagramm** (uPlot): Zeitraum 24 h / 7 T / 30 T / 90 T / 1 J, Wert
|
||||
wählbar, Tooltip mit exaktem Wert.
|
||||
4. **Tabelle „Projekte“**: sortierbar, Suchfeld, Δ-Spalten grün/rot mit Pfeil,
|
||||
Minikurve; Klick öffnet Detaildiagramm der App.
|
||||
5. **„Aufräumbar“**: reine Anzeige.
|
||||
- Aktualisiert sich jede Minute, solange sichtbar. Handy: Kacheln untereinander.
|
||||
- Neue Abhängigkeit: `uplot`. Lockfile wie bisher in einem Wegwerf-`node:20-alpine`
|
||||
erzeugen (Deploy-Image).
|
||||
|
||||
## Betrieb / Deploy
|
||||
|
||||
- Code: `/home/webklar/apps/serverpuls/` (eigenes Git-Repo, nur lokal).
|
||||
- `docker-compose.yml` (Backup vorher): Dienste `serverpuls` (`node:22-alpine`,
|
||||
`mem_limit: 128m`, `cpus: 0.5`, `init: true`; Host-`/proc` wird als `/host/proc:ro` gemountet,
|
||||
kein `pid: host`) und `serverpuls-docker-proxy`
|
||||
(`tecnativa/docker-socket-proxy`, nur lesend: `CONTAINERS=1`, `IMAGES=1`, `SYSTEM=1`
|
||||
für `/system/df`, `POST=0`), beide im `webklar-network`, ohne Traefik-Route.
|
||||
- Mounts serverpuls: `/proc:/host/proc:ro`, `/home/webklar/apps:/host/apps:ro`,
|
||||
`/var/lib/docker/volumes:/host/volumes:ro`, `serverpuls-data:/data`.
|
||||
- `.env`: `SERVERPULS_SMTP_USER=server@webklar.com`, `SERVERPULS_SMTP_PASS` – vom
|
||||
Nutzer über eine Erweiterung von `scripts/mail-zugang.py` gesetzt (Claude sieht das
|
||||
Passwort nicht).
|
||||
- WOMS-nginx: `location /api/serverpuls/` → `http://serverpuls:8091` (wie
|
||||
`/api/integrations/`).
|
||||
- WOMS-Code auf `feature/serverpuls` (Worktree `dev-serverpuls`, Basis `origin/test`).
|
||||
Live erst nach Push des Nutzers.
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
- Collector-Teilfehler (z. B. Proxy nicht erreichbar) → betroffene Werte fehlen, Rest
|
||||
wird gespeichert; Fehler im Log.
|
||||
- Größenmessung hat Zeitlimit (30 min); bricht sie ab, bleiben die Vortageswerte stehen
|
||||
und die Seite zeigt deren Datum.
|
||||
- SMTP-Fehler → `mail_queue`, erneuter Versuch alle 5 min bis 24 h.
|
||||
- Appwrite nicht erreichbar → API 502, Seite zeigt Fehlerhinweis.
|
||||
|
||||
## Tests
|
||||
|
||||
- Unit: Regeln (Schwellen, Dauerbedingungen, Hoch-/Herabstufung, Wiederholungssperre,
|
||||
behoben), Verdichtung, Wachstumsberechnung, `/proc`-Parser mit Beispieldateien.
|
||||
- API: gemocktes Appwrite (Admin → 200, ohne Label → 403, ohne JWT → 401).
|
||||
- UI: bewährtes esbuild-/Playwright-Harness unter RAM-Limit mit API-Fixtures.
|
||||
- Live-Check: Container-RAM < 128 MB, erste Messwerte, Test-Mail an server@.
|
||||
|
||||
## Nicht enthalten
|
||||
|
||||
- Keine Aktionen (Löschen, Neustarten) aus der Seite heraus.
|
||||
- Keine KI-Auswertung.
|
||||
- Keine automatischen Tickets (Warnungen nur per Mail und auf der Seite).
|
||||
@@ -0,0 +1,178 @@
|
||||
# Unterstützung bei Arbeitsblättern + 15-Minuten-Zeiten – Konzept
|
||||
|
||||
Stand 2026-09-30, freigegeben im Gespräch (Abschnitte 1–4).
|
||||
|
||||
## Ziel
|
||||
|
||||
Wer bei einem Arbeitsblatt eines Kollegen mitgeholfen hat, wird als **Unterstützung** eingetragen:
|
||||
Person und Tätigkeit. Die Person, die das Arbeitsblatt angelegt hat, behält 100 % der Zeit; jede
|
||||
unterstützende Person bekommt zusätzlich **50 % der Arbeitszeit, aufgerundet auf volle 15 Minuten**.
|
||||
Die Unterstützungszeit wird dem Kunden berechnet und im Kundenportal einzeln ausgewiesen.
|
||||
|
||||
Gleichzeitig werden Arbeitszeiten **überall nur noch in 15-Minuten-Schritten** erfasst. Das hebt die
|
||||
Entscheidung vom 2026-09-26 („ganze Minuten, keine 15er-Schritte“) auf.
|
||||
|
||||
Die Verträge (Abo-Bedingungen, AGB) halten Rundung und Unterstützung fest.
|
||||
|
||||
Nicht im Umfang: Auswertung der Stunden je Mitarbeiter, Umrechnung alter Arbeitsblätter,
|
||||
Rechnungserstellung.
|
||||
|
||||
## Rechenregel
|
||||
|
||||
```
|
||||
supportMinutes(totalTime) = ceil(totalTime / 2 / 15) * 15 // 0 bei totalTime <= 0
|
||||
worksheetMinutes(ws) = totalTime + Anzahl Unterstützer * supportMinutes(totalTime)
|
||||
```
|
||||
|
||||
Beispiele: 15 → 15, 30 → 15, 45 → 30, 60 → 30, 75 → 45, 70 (alt) → 45.
|
||||
Kommentare und Git-Pushs ohne nachgetragene Zeit haben `totalTime` 0, also auch 0 Unterstützung.
|
||||
|
||||
Die Regel steht in beiden Systemen (WOMS und Portal) mit denselben Testfällen. Die Minuten der
|
||||
Unterstützung werden **nie gespeichert**, sondern immer aus `totalTime` berechnet. Ändert sich die
|
||||
Arbeitszeit, stimmt die Unterstützung automatisch.
|
||||
|
||||
## Datenmodell
|
||||
|
||||
Appwrite `woms-database`, Collection `worksheets`, neues Attribut:
|
||||
|
||||
- `supporters` – String, nicht Pflicht, Größe 4000, JSON-Array:
|
||||
`[{ "employeeId": "<Appwrite-User-ID>", "name": "Andrej Stevanovski", "task": "Farbkonzept" }]`
|
||||
|
||||
Regeln: `task` ist Pflicht (1–200 Zeichen, getrimmt). Jede `employeeId` höchstens einmal. Die Person,
|
||||
die das Arbeitsblatt angelegt hat, darf nicht als Unterstützung stehen (bei Git-Pushs: der Mitarbeiter,
|
||||
dem der Push gehört, siehe `isWorksheetCreator`). `name` wird beim Eintragen gespeichert, damit die
|
||||
Zeile auch nach dem Löschen eines Mitarbeiters lesbar bleibt.
|
||||
|
||||
Das Attribut legt ein Skript `scripts/setup-supporters.mjs` an (idempotent). Der Nutzer führt es aus,
|
||||
weil Claude den Appwrite-Schlüssel nicht verwenden darf.
|
||||
|
||||
## WOMS (Ticketsystem)
|
||||
|
||||
### 15-Minuten-Schritte
|
||||
|
||||
Betroffen: `CreateWorksheetModal` (neues Arbeitsblatt), `WorksheetTimesModal` („Zeiten bearbeiten“ und
|
||||
„Zeit nachtragen“ bei Git).
|
||||
|
||||
- Das Feld „Arbeitszeit“ wird eine Auswahl: 15 min, 30 min, 45 min, 1 h, 1 h 15 … bis 12 h (48 Werte).
|
||||
Kein freies Eintippen mehr.
|
||||
- Beginn und Ende bleiben freie Uhrzeiten (bei Git ist das Ende die Uhrzeit des Pushs). Die
|
||||
automatisch berechnete Zeit (`minutesBetween`) wird auf den nächsten 15-Minuten-Schritt
|
||||
**aufgerundet**; danach kann ein anderer Wert gewählt werden. Dauer 0 → 15 min vorausgewählt.
|
||||
- Hat ein altes Arbeitsblatt eine Zeit außerhalb des Rasters (z. B. 70), ist beim Bearbeiten der
|
||||
nächsthöhere Schritt (75) vorausgewählt.
|
||||
- Beim Speichern wird geprüft, dass `totalTime` ein positives Vielfaches von 15 ist (außer bei
|
||||
„Nur Kommentar“, dort 0).
|
||||
- `useWorksheets.createWorksheet` rechnet eine fehlende Zeit ebenfalls aufgerundet.
|
||||
|
||||
### Unterstützung eintragen
|
||||
|
||||
**Person, die das Arbeitsblatt angelegt hat** (`canEditWorksheetTimes`): im Formular „Zeiten
|
||||
bearbeiten“ und beim neuen Arbeitsblatt der Haken **„Unterstützung“**. Darunter Zeilen mit
|
||||
*Mitarbeiter* (Auswahl aus `employees`, ohne sich selbst und ohne schon gewählte) und *Wobei?*
|
||||
(Pflichttext), „+ weitere Person“, Entfernen je Zeile. Haken aus = Liste leeren.
|
||||
|
||||
**Alle anderen Mitarbeiter:** bei fremden Arbeitsblättern mit Arbeitszeit der Knopf
|
||||
**„Ich habe unterstützt“**. Kleines Formular nur mit *Wobei?*. Ist man schon eingetragen, heißt der
|
||||
Knopf „Meine Unterstützung bearbeiten“ und man kann die Tätigkeit ändern oder den Eintrag entfernen.
|
||||
Andere Einträge und die Zeiten sind nicht änderbar.
|
||||
|
||||
Neue Hilfen in `lib/ticketForm.js` (oder `lib/support.js`): `supportMinutes`, `worksheetMinutes`,
|
||||
`parseSupporters` (fehlertolerant, kaputtes JSON → leere Liste), `canAddOwnSupport(ws, user, employee)`
|
||||
(nicht Ersteller, Arbeitsblatt hat Arbeitszeit), `upsertOwnSupport`, `removeOwnSupport`.
|
||||
|
||||
Unterstützung ist nur möglich bei Arbeitsblättern mit Arbeitszeit: nicht bei reinen Kommentaren, bei
|
||||
Git-Pushs erst nach „Zeit nachtragen“.
|
||||
|
||||
Speichern: `updateWorksheet` patcht `{ supporters }`. „Ich habe unterstützt“ liest das Dokument
|
||||
vorher frisch und schreibt nur den eigenen Eintrag um, damit gleichzeitige Einträge anderer nicht
|
||||
überschrieben werden.
|
||||
|
||||
Berechtigungen: wie bisher nur in der Oberfläche durchgesetzt (die Collection erlaubt Update für
|
||||
Benutzer). Keine Änderung an den Appwrite-Rechten.
|
||||
|
||||
### Anzeige
|
||||
|
||||
- `WorksheetList`: unter dem Arbeitsblatt je Unterstützung
|
||||
„↳ Unterstützung Andrej · Farbkonzept · 30 min“.
|
||||
- `WorksheetSummary` „Arbeitszeit gesamt“ und `useWorksheets` Summen verwenden `worksheetMinutes`.
|
||||
- Der Abo-Haken („+ Abo“, `countsToPlan`) gilt für das ganze Arbeitsblatt einschließlich Unterstützung.
|
||||
|
||||
## Kundenportal
|
||||
|
||||
- Gleiche Rechenregel als `server/services/worksheetMinutes.js` (oder vorhandenes Modul) mit
|
||||
`supportMinutes`, `worksheetMinutes`, `parseSupporters`. Kaputtes JSON → Eintrag überspringen und
|
||||
loggen, nie Absturz.
|
||||
- **Stundenkonto** (`hourLedger.js`): Verbrauch eines Abo-Arbeitsblatts = `worksheetMinutes(ws)`
|
||||
statt `totalTime`. Die Zeile im Verlauf bekommt den Zusatz „inkl. 30 min Unterstützung“.
|
||||
- **„Arbeiten am Projekt“** (`customerProjectView.js`, `public/app.js`): jeder Eintrag bekommt
|
||||
`supports: [{ name, task, minutes }]`, dargestellt als eigene Zeilen unter dem Schritt:
|
||||
„↳ Unterstützung Andrej: Farbkonzept · 30 min“. Werden mehrere Git-Pushs eines Tages zu
|
||||
„Website aktualisiert“ zusammengefasst, hängen alle Unterstützungen unter diesem Eintrag.
|
||||
- **Gesamtzeit oben:** im Kopf der Projektdetails (neben Auftragsnummer und Status) die Zeile
|
||||
„Arbeitszeit gesamt: 4 h 30 min“ = Summe aller Schritte inkl. Unterstützung. Ohne Auftrag steht sie
|
||||
oben im Abschnitt „Arbeiten am Projekt“. Bei 0 wird sie nicht gezeigt.
|
||||
- `customerActivity.js` gibt `supporters` mit weiter, wo `totalTime` weitergegeben wird.
|
||||
- **Push-Zeitmail** (Spec af7ecce, noch nicht gebaut): „andere Zeit“ nur in 15-Minuten-Schritten;
|
||||
wird in jener Spec nachgetragen.
|
||||
|
||||
## Verträge
|
||||
|
||||
Stand heute: Abo-Bedingungen Version 4 (live seit 2026-09-30) § 4 enthält schon „aufgerundet auf volle
|
||||
15 Minuten“ / „in Schritten von 15 Minuten“, aber nichts zur Unterstützung. AGB § 9 sagt nur
|
||||
„Die Vergütung ergibt sich aus dem Angebot“.
|
||||
|
||||
- **Abo-Bedingungen Version 5**, § 4 neuer Satz nach der Rundungsregel (Entwurf):
|
||||
„Wirkt an einem Arbeitsschritt eine weitere Person unterstützend mit, etwa bei Gestaltung,
|
||||
Abstimmung oder Prüfung, rechnen wir für sie die Hälfte der Arbeitszeit dieses Arbeitsschritts an,
|
||||
aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede von ihnen.
|
||||
Im Kundenportal weisen wir jeden Arbeitsschritt mit seiner Arbeitszeit und jede Unterstützung mit
|
||||
Person, Tätigkeit und angerechneter Zeit einzeln aus.“
|
||||
- **AGB § 9** neuer Absatz 2, nur für Abrechnung nach Aufwand (Festpreis-Angebote unberührt), Regel
|
||||
vollständig ausgeschrieben (kein Verweis auf die Abo-Bedingungen):
|
||||
„Wird nach Aufwand abgerechnet, erfassen wir die Arbeitszeit je Arbeitsschritt in Schritten von
|
||||
15 Minuten; angefangene 15 Minuten runden wir auf. Wirkt eine weitere Person unterstützend mit,
|
||||
rechnen wir für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle
|
||||
15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede
|
||||
Unterstützung ist im Kundenportal einsehbar.“
|
||||
Wird mit dem offenen § 11-Haftungsentwurf (webklar.com f40e889, fix/agb-haftung) zu **einer** neuen
|
||||
AGB-Version gebündelt.
|
||||
- Alle Stellen, die die Texte wiederholen (PDF-Anhang der Vertragsmails, `vertragsunterlagen/`,
|
||||
Kalkulation, Portal-Texte), werden abgeglichen. Beide Texte kommen in WOMS „Rechtliches“ als neue
|
||||
Version.
|
||||
- Laufende Abos bleiben auf ihrer Version. Einschätzung (keine Rechtsberatung): Version 4 rechnet die
|
||||
„tatsächlich aufgewendete Zeit“ ab; bei zwei Personen fällt mehr Zeit an, als mit 50 % berechnet
|
||||
wird, die Unterstützung ist also gedeckt.
|
||||
- Die endgültigen Texte legt Claude dem Nutzer vor dem Livegang einzeln zur Freigabe vor.
|
||||
|
||||
## Alte Daten und Fehlerfälle
|
||||
|
||||
- Alte Arbeitsblätter bleiben unverändert (keine Massen-Umrechnung); 70 Minuten bleiben 70 Minuten,
|
||||
bis jemand das Blatt bearbeitet.
|
||||
- Ohne `supporters` = keine Unterstützung.
|
||||
- Gelöschter Mitarbeiter: gespeicherter Name bleibt, Zeile wird weiter gezeigt und berechnet.
|
||||
- Doppelte Person / Ersteller als Unterstützung: Oberfläche verhindert es; beim Lesen werden Duplikate
|
||||
ignoriert.
|
||||
- Arbeitszeit 0 oder „Nur Kommentar“: Unterstützung zählt 0 (Einträge bleiben gespeichert).
|
||||
|
||||
## Livegang (Reihenfolge)
|
||||
|
||||
1. Nutzer führt `scripts/setup-supporters.mjs` aus (Attribut `supporters`).
|
||||
2. Kundenportal live (Push `main`), damit keine Unterstützung eingetragen werden kann, die der Kunde
|
||||
nicht sieht.
|
||||
3. WOMS live (Push `feature/unterstuetzung:test`).
|
||||
4. Verträge: Abo-Bedingungen v5 (Portal) und AGB (webklar.com) nach Freigabe der Texte; Versionen in
|
||||
„Rechtliches“ eintragen.
|
||||
|
||||
Nichts wird ohne das OK des Nutzers gepusht.
|
||||
|
||||
## Tests
|
||||
|
||||
- Rechenregel in beiden Systemen mit denselben Fällen (15, 30, 45, 60, 75, 70, 0, Kommentar).
|
||||
- WOMS: Unit-Tests (`npm test`) für Rechnung, `parseSupporters`, Berechtigungen
|
||||
(`canAddOwnSupport`, Ersteller/Nicht-Ersteller, Git-Push), eigener Eintrag ändern/entfernen.
|
||||
Playwright mit nachgebautem Appwrite: 15-Minuten-Auswahl und Aufrunden, Haken „Unterstützung“ mit
|
||||
zwei Personen, „Ich habe unterstützt“, Anzeige in Liste und Summe.
|
||||
- Portal: Tests für `customerWorkEntries` (Unterstützungs-Zeilen, zusammengefasste Git-Tage),
|
||||
Gesamtzeit, Stundenkonto mit Unterstützung, kaputtes JSON.
|
||||
- Vor dem Livegang: Build wie beim Deploy (`docker run node:20-alpine`), Blick auf echte Daten nur
|
||||
lesend im Chrome des Nutzers.
|
||||
@@ -0,0 +1,541 @@
|
||||
# WOMS „Weißes Design“ – Konzept
|
||||
|
||||
Stand 2026-10-04. Freigegeben im Gespräch:
|
||||
- Prototyp angesehen, Akzentfarbe „Schwarz passt“.
|
||||
- Ticket öffnet als eigene Seite.
|
||||
- Paketablauf und alle Standardentscheidungen mit „ja passt“.
|
||||
|
||||
Danach gegen den Code geprüft: 4 Prüfer, Gegenprüfung jedes Befunds, 66 bestätigte Befunde eingearbeitet.
|
||||
|
||||
Grundlage:
|
||||
- Klick-Prototyp im Repo `WEBklar/ticketsystem-design`, Vorschau `https://ticketsystem-design.project.webklar.com` mit Mitarbeiter-Login. Kopie unter `/root/reports/ticketsystem-design-2026-10-04/quelle/`.
|
||||
- Bestandsaufnahme aller 104 Quelldateien in `2026-10-04-weisses-design-umbaukarte.md` (gleicher Ordner). **Bei Widersprüchen gilt dieses Dokument.**
|
||||
|
||||
## Ziel
|
||||
|
||||
Das ganze Ticketsystem (ticket.webklar.com) bekommt ein neues Aussehen:
|
||||
- steriles Weiß, sehr minimalistisch,
|
||||
- kaum Rahmen, kaum Icons,
|
||||
- angelehnt an das Stripe-Dashboard und trotzdem übersichtlich.
|
||||
|
||||
Das dunkle Design entfällt vollständig. Es gibt keinen Umschalter.
|
||||
|
||||
Der Umbau ist **rein visuell**, mit zwei Ausnahmen:
|
||||
- die in „Bewusste Änderungen am Verhalten“ genannten Punkte,
|
||||
- vor allem: Ein Ticket öffnet als **eigene Seite** mit eigenem Link (`/tickets/<Nummer>`), statt in der Liste aufzuklappen.
|
||||
|
||||
Erfolg heißt:
|
||||
- Jede Seite folgt den Gestaltungsregeln.
|
||||
- Keine heutige Funktion geht verloren, außer den ausdrücklich genannten.
|
||||
- Alle Tests sind grün.
|
||||
- Jedes Paket geht einzeln live.
|
||||
- Das jüngste Paket lässt sich per Revert zurücknehmen.
|
||||
|
||||
Nicht im Umfang:
|
||||
- Dark Mode, neue Funktionen
|
||||
- Änderungen am Datenmodell, an Rechten oder am Server
|
||||
- das Kundenportal, auch nicht dessen Chat-Composer
|
||||
- Terminplaner TP2
|
||||
- flächige Rechtschreibkorrekturen (ae/ue) in Bestandstexten
|
||||
|
||||
## Gestaltungsregeln
|
||||
|
||||
1. **Weiß überall.**
|
||||
- Seite, Menü und Dialoge haben `#ffffff`.
|
||||
- Es gibt keine Verläufe, Leuchteffekte, Glas-Effekte oder Hintergrundanimationen.
|
||||
- Einzige Ausnahme sind Bildansicht und Kamera des fremden Chat-Composers. Sie bleiben dunkel, weil sie Medienbetrachter sind.
|
||||
2. **Kaum Rahmen.**
|
||||
- Struktur entsteht durch Weißraum, Schriftgrößen und einzelne 1-px-Haarlinien `#ebeef1`.
|
||||
- Haarlinien stehen zwischen Tabellen- und Listenzeilen, unter Reitern, rechts neben dem Menü, zwischen Schlüsselwerten (`.fact`) und zwischen den Tagesspalten im Planboard.
|
||||
- Karten oder Kästen um Inhalte gibt es nicht.
|
||||
- Eingabefelder und Zweitknöpfe tragen statt Rahmen einen feinen Ring-Schatten.
|
||||
3. **Kaum Icons.**
|
||||
- Menü, Knöpfe, Überschriften und Badges tragen keine Icons.
|
||||
- Handelnde Zeichen werden als Schriftzeichen gesetzt, nicht als Icon-Komponente: Schließen „ד, Pfeile „← →“, Sortierung „↑ ↓“.
|
||||
- Am Ende fliegen `react-icons` und `@tabler/icons-react` aus `package.json`.
|
||||
4. **Eine Akzentfarbe: Schwarz `#111111`.**
|
||||
- Sparsam eingesetzt: Primärknöpfe, aktiver Reiter, aktiver Menüpunkt, heutige Datumspille im Planboard.
|
||||
- Pro Seitenkopf höchstens ein schwarzer Knopf.
|
||||
- Links sind am feinen Unterstrich erkennbar, nicht an der Farbe.
|
||||
5. **Statusfarben** sind leicht getönte Pillen ohne Rahmen: Radius 4 px, Höhe 20 px, Schrift 12 px/500.
|
||||
|
||||
| Ton | Hintergrund | Schrift | Ticketstatus (gespeicherte Werte) |
|
||||
|---|---|---|---|
|
||||
| grau | #ebeef1 | #414552 | Offen (`Open`), leer oder unbekannt |
|
||||
| blau | #e0f2fe | #075985 | In Arbeit (`Occupied`, `Assigned`, `Added Info`, `In Kontakt`) |
|
||||
| orange | #fcedb9 | #a82c00 | Wartet auf Kunde (`Awaiting`, `Halted`) |
|
||||
| violett | #efeafe | #5b21b6 | Prüfung (`In Test`) |
|
||||
| grün | #d7f7c2 | #006908 | Erledigt (`Closed`, `Zugesagt`) |
|
||||
| plain | – | #5e6678 | Storniert (`Cancelled`, `Aborted`, `Abgesagt`) |
|
||||
| rot | #ffe7f2 | #b3093c | kein Status: Badge „Dringend“ |
|
||||
|
||||
6. **Schrift.**
|
||||
- Systemschrift `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`. Lato und `src/assets/fonts` entfallen.
|
||||
- Größen: h1 28/34 700 (Handy 24/30), Abschnitt-h2 16/24 600, Dialogtitel 18/26 600, Text 14/20, klein 13, sehr klein 12/16, Kennzahl 22/30 600.
|
||||
- Gewicht 800 entfällt. Großbuchstaben mit Sperrung gibt es nur noch für die Wochentage im Planboard.
|
||||
- Zahlen und Beträge nutzen `tabular-nums`.
|
||||
7. **Breakpoints der WOMS-Seiten: 1100 px und 860 px.**
|
||||
- Unter 1100 px werden Nebenspalten ausgeblendet (`.hide-md`).
|
||||
- Unter 860 px:
|
||||
- Das Menü wird zur Schublade mit Menü-Knopf.
|
||||
- Seitenköpfe stapeln sich.
|
||||
- Tabellen zeigen nur Kernspalten (`.hide-sm`).
|
||||
- Dialoge erscheinen als Bottom-Sheet.
|
||||
- Der Chat wechselt zwischen Liste und Verlauf (heute 720 px).
|
||||
- Ausgenommen:
|
||||
- die Container-Queries der Finanztabellen (560/640/860)
|
||||
- die Breakpoints des fremden Chat-Composers (640/880)
|
||||
8. **Lesbarkeit.**
|
||||
- Text `#1a1f36`, Nebentext `#5e6678` (5,8:1).
|
||||
- Tertiärtext `#8a93a3` (3,1:1) nur für Nicht-Essenzielles, nämlich Nummern, „–“ und leere Zustände. Inhalte und Status nutzen ihn nie.
|
||||
- Fehlerrot `#c0123c`.
|
||||
- Fokus ist immer sichtbar:
|
||||
- Knöpfe und Links: 2 px durchgezogen `#111111` mit 2 px Abstand.
|
||||
- Felder: 1 px `#111111` plus Ring `rgba(17,17,17,.14)`.
|
||||
- Deaktiviert: Deckkraft .55, Cursor `not-allowed`. Schreibgeschützte Felder bekommen den Hintergrund `#f8f9fb`.
|
||||
|
||||
## Design-Tokens
|
||||
|
||||
Die Tokens stehen in `:root` in `src/styles/global.css`. Quelle ist der Prototyp mit festem Akzent, ohne `html[data-accent]` und ohne Prototyp-Leiste.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg: #ffffff; --bg-hover: #f6f8fa; --bg-subtle: #f8f9fb;
|
||||
--text: #1a1f36; --text-2: #5e6678; --text-3: #8a93a3;
|
||||
--line: #ebeef1; --line-strong: #d8dde3;
|
||||
--accent: #111111; --accent-hover: #2b2b2b; --accent-ink: #111111;
|
||||
--accent-soft: rgba(17,17,17,.06); --accent-ring: rgba(17,17,17,.14);
|
||||
--grey-bg: #ebeef1; --grey-fg: #414552; --blue-bg: #e0f2fe; --blue-fg: #075985;
|
||||
--orange-bg: #fcedb9; --orange-fg: #a82c00; --violet-bg: #efeafe; --violet-fg: #5b21b6;
|
||||
--green-bg: #d7f7c2; --green-fg: #006908; --red-bg: #ffe7f2; --red-fg: #b3093c;
|
||||
--danger: #c0123c; --danger-ring: rgba(192,18,60,.18);
|
||||
--radius: 6px; --radius-lg: 12px;
|
||||
--ring: 0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06);
|
||||
--shadow-btn: 0 1px 1px rgba(0,0,0,.12), 0 2px 5px rgba(60,66,87,.08);
|
||||
--shadow-pop: 0 0 0 1px rgba(60,66,87,.06), 0 12px 32px rgba(26,31,54,.14), 0 2px 6px rgba(26,31,54,.06);
|
||||
--overlay: rgba(26,31,54,.32); --scrim: rgba(26,31,54,.24);
|
||||
--nav-dot: #e5a000;
|
||||
--appt-akquise: #fdf4e3; --appt-akquise-hover: #fbeccd; --appt-hover: #eef1f4;
|
||||
--sidebar-w: 232px;
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
html { color-scheme: light; }
|
||||
```
|
||||
|
||||
**Alte Token-Namen bleiben bis P10 als Aliase.** Fremd-CSS und JSX benutzen sie noch, so wird Bestandscode sofort weiß.
|
||||
|
||||
| Alter Name | Neuer Wert |
|
||||
|---|---|
|
||||
| `--surface-1`, `--surface-3`, `--surface`, `--dark-bg`, `--dark-card-bg` | `--bg` |
|
||||
| `--surface-2`, `--surface-hover` | `--bg-hover` |
|
||||
| `--border`, `--gray-700`, `--admin-line` | `--line` |
|
||||
| `--border-strong`, `--admin-line-strong` | `--line-strong` |
|
||||
| `--border-accent` | `--accent-ring` |
|
||||
| `--text-muted`, `--text-faint`, `--gray-800` | `--text-2` (Altcode setzt `--text-faint` auch für Inhalte ein) |
|
||||
| `--dark-text`, `--gray-900` | `--text` |
|
||||
| `--accent-2`, `--green-secondary` | `--accent-hover` |
|
||||
| `--green-primary` | `--accent` |
|
||||
| `--info` | `--blue-fg` |
|
||||
| `--warn` | `--orange-fg` |
|
||||
| `--ok` | `--green-fg` |
|
||||
| `--r-sm` | `--radius` |
|
||||
| `--r-md`, `--r-lg` | `--radius-lg` |
|
||||
| `--r-pill` | 4px |
|
||||
| `--shadow-1` | `none` |
|
||||
| `--shadow-2` | `--shadow-btn` |
|
||||
| `--shadow-3` | `--shadow-pop` |
|
||||
| `--sp-1…6` | bleiben unverändert |
|
||||
| `--sidebar-w-collapsed` | entfällt |
|
||||
|
||||
**Diagrammfarben.** Canvas kann kein `var()` lesen. Deshalb stehen die Farben der Diagramme (uPlot) als Konstanten in der neuen Datei `src/components/serverpuls/chartColors.js`. Sie wird schon in P1 angelegt und von `Chart.jsx` und `ServerpulsPage` genutzt.
|
||||
|
||||
| Element | Farbe |
|
||||
|---|---|
|
||||
| Linie | `#111111` |
|
||||
| Füllung | `rgba(17,17,17,.06)` |
|
||||
| Spitze | `#8a93a3`, gestrichelt |
|
||||
| Achsen | `#5e6678` |
|
||||
| Raster | `#ebeef1`, nur horizontal |
|
||||
| Ticks | `#d8dde3` |
|
||||
|
||||
## Bausteine
|
||||
|
||||
Die Klassennamen folgen dem Prototyp (`styles.css`). Wo eine Komponente existiert, behält sie ihre Schnittstelle, außer wo unten anders vermerkt. Es ändern sich Markup und Klassen.
|
||||
|
||||
| Baustein | Umsetzung |
|
||||
|---|---|
|
||||
| **App-Gerüst** | `.shell` mit Menü `<aside class="sidebar">` (fest, 232 px) und `.main`. In `.main` liegen `.mobile-bar` und `<main id="inhalt">` mit den Routen, ohne eigene Klasse `.page`. Jede Seite behält ihre Wurzel mit Klasse `page`, die den Prototyp-Look bekommt (max. 1160 px, Abstand 36/48/80). `ProjectsPage` wechselt in P1 von `main-content` auf `page`. `AdminPage` wechselt in P1 von `<main className="page admin-page">` auf `<div className="page admin-page">`, die maximale Breite 1480 px von `.admin-page` bleibt. Gescrollt wird das Dokument, ein innerer Scroll-Container entfällt. Der Footer entfällt, sein Text steht künftig im Login-Fuß. Unter 860 px ist das Menü eine Schublade mit `.scrim`. Sie schließt bei Klick daneben, bei Esc (Listener nur aktiv, solange sie offen ist) und bei Wechsel des Pfads. Die Seite springt nur bei Wechsel von `location.pathname` nach oben, nie bei Änderungen am Suchteil der URL. |
|
||||
| **Menü** | Reine Textliste `.nav-item` mit Unterpunkten `.sub` und Zählern `.nav-count`. Chat-Ungelesene bekommen `.hot` (neues Feld `hot` am Chat-Eintrag in `navLinks.js`). „Leads“ erscheint grau mit „pausiert“. Serverpuls-Punkt: `.nav-dot` gelb bei `medium`, `.nav-dot.critical` rot bei `critical`. `data-testid="sp-nav-dot"`, Titel, 5-Minuten-Abfrage und `useSidebarCounts` bleiben. Fuß `.sidebar-foot`: Name (gekürzt mit Ellipsis) und „Abmelden“. Das Einklappen entfällt samt `lib/sidebar.js` und Test. Reihenfolge, Texte und Ziele in `navLinks.js` bleiben. |
|
||||
| **Logo** | Neue `components/Logo.jsx` mit dem SVG-Zeichen aus dem Prototyp, 24 px im Menü und in der Mobil-Leiste, 40 px im Login. Dazu `public/favicon.svg` und `<link rel="icon">` in `index.html`. `<title>` bleibt. |
|
||||
| **Seitenkopf** | `PageHeader` rendert `.page-head` mit h1, `.sub` und `.actions`. Neue optionale Props `eyebrow` und `crumb`. Brotkrumen `.crumb`, z. B. „Kunden / Name“. |
|
||||
| **Abschnitt** | `Card` rendert `<section>` mit `<h2 class="section">`: Titel links, Aktion rechts, ohne Hintergrund, Rand, Schatten und Innenabstand. `pad`, `bodyStyle` und `style` wirken nicht mehr. `className` bleibt, Finanzen braucht es für Container-Queries. |
|
||||
| **Knöpfe** | `Button`: Standard und `ghost` werden `.btn` (weiß mit Ring), `primary` wird `.btn.btn-primary` (schwarz). Neu ist `quiet` als `.btn.btn-quiet`. `danger` wird `.link-danger` (roter Textlink). Die Prop `size` wird angenommen, wirkt aber nicht: Alle Knöpfe sind 32 px hoch, nur der Login-Knopf 36 px. Zeilenaktionen in Tabellen sind Textlinks `.link`. Während einer Anfrage sind Knöpfe `disabled` und zeigen Text („Speichert …“), keinen Spinner. |
|
||||
| **Links** | `a.link` mit feinem Unterstrich (`--line-strong`, beim Überfahren `currentColor`). Basisregel `a:not([class])` mit demselben Unterstrich, damit auch klassenlose Links (Rechtstext-Dateien, AVV-Link der Analyse-Zustimmung u. a.) erkennbar bleiben. Neuer Code vergibt immer eine Klasse. |
|
||||
| **Badge** | `<span class="badge {ton}">`. Die Ton-Namen der Logik (`muted, info, ok, warn, danger`) bleiben, Tests pinnen sie. `Badge.jsx` übersetzt: muted → grau, info → blau, ok → grün, warn → orange, danger → rot. Neu sind `violet` und `plain`. Die Prop `dot` entfällt. |
|
||||
| **Ticketstatus** | Neue Funktion `statusTone(status)` in `lib/ticketStatus.js` mit den Rückgaben `'' \| 'blue' \| 'orange' \| 'violet' \| 'green' \| 'plain'` nach der Tabelle oben. Leer, `undefined` und unbekannt ergeben `''`. Der Test deckt jeden gespeicherten Wert ab. `StatusPill` rendert `badge {ton}` direkt, ohne das Mapping in `Badge.jsx`. `UrgentPill` rendert „Dringend“ in Rot. `statusClass` entfällt mit P2. |
|
||||
| **Reiter** | `Tabs` rendert `.tabs` und `.tab` mit 2-px-Unterstrich und optionalem Zähler `<span class="count">`. Auf dem Handy scrollen die Reiter waagerecht. Zähler werden beim Rendern ergänzt, nie in `lib/customerTabs.js`. |
|
||||
| **Umschalter** | `.seg`: graue Schiene, der aktive Teil ist weiß mit Ring, dazu `aria-pressed`. |
|
||||
| **Tabelle** | `.table` mit Kopf 12 px/500 und Haarlinien. `tr.row` ist klickbar und wird beim Überfahren `--bg-subtle`. Weitere Klassen: `.cell-title`, `.cell-sub`, `.id`, `.right`, `.num`, `.table-foot`, `.hide-md`, `.hide-sm`. Die erste Zelle enthält immer ein echtes Bedienelement für Tastatur und Mittelklick: einen Link oder, wenn die Zeile einen Dialog öffnet, einen `<button class="link">`. Klicks auf `a`, `button`, `select`, `input` und `label` in der Zeile lösen keine Zeilenaktion aus. |
|
||||
| **Liste** | `.list-row` mit Haarlinie. Bei Link-Zeilen färbt sich beim Überfahren der Titel. |
|
||||
| **Schlüsselwerte** | `.facts`/`.fact` für den Ticketkopf. `.stats`/`.stat` für Kennzahlen: `.v.warn` orange, neu `.v.neg` rot für negative Beträge. `.kv` für Beschriftung und Wert, mobil einspaltig. Dazu `.two-col` und `.prose` (Fließtext mit Zeilenumbrüchen). Neue Komponente `Stat` mit Prop `tone`, ohne Icon und ohne Farbstreifen. `Kpi` bleibt bis P7 als Export erhalten und rendert `Stat`: `accent` = `var(--danger)` wird `neg`, alle anderen `accent` werden ignoriert. |
|
||||
| **Formular** | `.field > label` (500) mit `.input`, `.select` oder `.textarea` (32 px, Ring statt Rand, zweistufiger Fokus) und `.hint`. Mehrspaltige Formulare nutzen `.field-row` und `.field-row-3`, Kästchen `.check`. Fehler zeigt ein roter Ring (`--danger`, `--danger-ring`), auch bei `[aria-invalid="true"]`. `Field`, `FormField`, `DateTimeInput`, `DurationSelect` und `AutocompleteInput` übernehmen die Klassen. Die Vorschlagsliste ist ein weißes Popover mit `--shadow-pop`. |
|
||||
| **Dialog** | Neue `ui/Dialog.jsx` mit den Props `title`, `onClose`, `busy`, `wide`, `footer`. Aufbau: `.overlay` (`--overlay`) mit `.dialog` (weiß, Radius 12, `--shadow-pop`, 520 px, mit `wide` 760 px). Der Kopf enthält h3 und den Knopf „ד (`aria-label="Schließen"`), danach folgen `.dialog-body` (scrollt) und `.dialog-foot`. Attribute: `role="dialog"`, `aria-modal`, `aria-labelledby`. Den Fokus regelt `useDialogFocus`. Unter 860 px erscheint der Dialog als Bottom-Sheet. **Schließen geht nur über × und Esc.** Ein Klick auf den Hintergrund schließt nicht, damit lange Eingaben nicht verloren gehen (Abweichung vom Prototyp). Esc greift nur, wenn `!e.defaultPrevented`, so schließt eine offene Vorschlagsliste zuerst sich selbst. Solange `busy` gilt, wirken × und Esc nicht, und × ist `disabled`. Beide Wege rufen genau das `onClose` des Aufrufers. `AssignDialog` übergibt sein `close()` samt `refresh`. Die eigenen Esc-Listener in `InvoiceCreateModal` und `AssignDialog` entfallen. `lockRef` und `busyRef` bleiben als Doppelklick-Sperre. Die native `required`-Prüfung bleibt. |
|
||||
| **Leerer Zustand** | `<div class="empty"><strong>Titel</strong>Hinweis</div>` mit optionaler Aktion. Die Prop `icon` entfällt. |
|
||||
| **Meldungen** | Ohne Kasten: `.msg.err` (rot, `role="alert"`), `.msg.ok` (grün), `.msg.warn` (orange), alle 13 px. Flüchtige Bestätigungen dürfen `.toast` nutzen. |
|
||||
| **Laden** | `.loading` mit Text „Wird geladen …“. `AdminRoute` behält wörtlich `<p>Wird geladen …</p>` ohne Klasse, weil ein Test darauf prüft. Der Seitenabstand kommt dort vom Elternelement. |
|
||||
| **Dringend-Schalter** | `UrgentToggle` rendert den kleinen Schalter `.sw` mit dem Wort „Dringend“. Eingeschaltet ist er rot. `aria-pressed` und die Logik bleiben. |
|
||||
| **Login** | Aufbau: `.login` > `.login-box` (360 px), Logo, h1 „Anmelden“, `.sub` „Ticketsystem für das WEBklar-Team“, Felder „E-Mail“ und „Passwort“ mit `autocomplete`, Knopf „Weiter“. Danach die Schritte „Code senden“ und „Code eingeben“ (h1 „Code eingeben“, `.code-input`, Links „Code erneut senden“ · „Abbrechen“). Die erklärenden Texte der Code-Schritte bleiben wörtlich wie heute: „Wir schicken dir einen Code an deine E-Mail-Adresse.“, „Wir haben dir einen Code an {E-Mail} geschickt.“, „Code aus der E-Mail“, „Neuer Code ist unterwegs.“. Die Prototyp-Hinweise zum Testbetrieb kommen nicht ins echte System. `.login-foot` zeigt „Webklar Ticket-Plattform · © {Jahr}“. Die Logik (`login`, `sendCode`, `verifyCode`, `cancelMfa`, `NO_STAFF_ACCESS`, `safeReturnPath`) bleibt unverändert. `LetterGlitch.jsx` entfällt. |
|
||||
|
||||
### Übergangsschicht KOMPAT (P1 bis P10)
|
||||
|
||||
Ein markierter Block „KOMPAT“ in `global.css` bildet alte Klassen auf den neuen Look ab. So sind noch nicht umgebaute Seiten weiß, kastenlos und lesbar. Er gilt von P1 bis P10.
|
||||
|
||||
| Alte Klasse | Darstellung im neuen Look |
|
||||
|---|---|
|
||||
| `.ui-card*` | Abschnitt ohne Kasten, `.ui-card-head h3` wie `h2.section` |
|
||||
| `.ui-btn*` | wie `.btn` / `.btn-primary` / `.link-danger` |
|
||||
| `.form-group`, `.form-label`, `.form-control` | wie `.field`, Label, `.input` / `.select` / `.textarea` |
|
||||
| `.badge-*`, `.status-*`, `.priority-*` | Badge-Töne |
|
||||
| `.overlay` (Altform) | behält `overflow-y: auto`, `align-items: flex-start` und 24/16 px Abstand, Hintergrund `--overlay` |
|
||||
| `.overlay-content` | weiße Box: `width: 100%`, `max-width: 520px` (Inline-`maxWidth` der Aufrufer wirkt weiter, `.fin-dialog` 760 px), `margin: 0 auto`, Radius 12, `--shadow-pop`, Abstand 24, Schrift `--text` |
|
||||
| `.overlay-close` | fest oben rechts im Fenster, 32×32 px, weißer runder Knopf mit „ד in `--text-2` |
|
||||
| `.seg button.active` | wie `.seg button.on` |
|
||||
| `.bg-red` (mit oder ohne `.text-white`) | kein Hintergrund, Schrift `--red-fg`, 13 px |
|
||||
| `.text-white` allein | keine Regel |
|
||||
| `.text-grey` | `--text-2` |
|
||||
| `.text-red` | `--red-fg` |
|
||||
| `.side-count` | kleine graue Zahl, bis P6 (Chat-Liste) |
|
||||
| `.side-logo` | Initiale ohne Verlauf, bis P4 (Kundenliste) |
|
||||
| `.btn-dark`, `.btn-teal` | wie `.btn-primary` |
|
||||
| `.card*`, `.row`, `.col*`, `.dropdown*` | neutral |
|
||||
| `.spinner` | neutraler grauer Ring |
|
||||
| `.page .page`, `.page .main-content` | Abstand und Breite neutralisiert, falls eine Seite verschachtelt |
|
||||
| Links in Bestandsklassen, die bisher `--accent` als Farbe nutzten (`.fin-link`, `.legal-files a`, `.credit-cols a`, `.area-rows a`) | feiner Unterstrich |
|
||||
|
||||
Alle `!important`-Regeln des Altbestands werden gelöscht, nicht überschrieben. Tote Klassen löscht P1 nur nach einer Liste aus einem Abgleich-Skript:
|
||||
- Das Skript vergleicht alle Klassen in `global.css` mit Klassen-Literalen in JSX, mit Template-Präfixen (z. B. `fin-fixed-${…}`, `legal-tone-${…}`) und mit Zeichenketten in `lib/*.js`.
|
||||
- Gelöscht wird erst nach Gegenprobe mit `rg -w <name> src --glob '!*.css'`.
|
||||
- Dynamisch gebaute Klassen gelten nicht als tot. Beispiele: `fin-neg`, `fin-fixed-off`, `legal-tone-*`, `legal-version-*`, `badge-*`, `status-*`, `ticket-tab-active`, `ui-btn-*`.
|
||||
- Die Liste steht im Plan.
|
||||
|
||||
P10 entfernt den KOMPAT-Block.
|
||||
|
||||
## Ticket als eigene Seite
|
||||
|
||||
### Route und Links
|
||||
|
||||
- Route `/tickets/:ref/:tab?`.
|
||||
- Sie ist wie alle anderen eine `ProtectedRoute` und steht vor dem Fallback `*`.
|
||||
- `tab` ist `details` (Standard) oder `arbeitsblaetter`. Ein unbekannter `tab` leitet mit `replace` auf Details um.
|
||||
- Das Menü markiert „Tickets“ auch auf Unterseiten.
|
||||
- Neue `lib/ticketRoutes.js` mit `ticketPath(ref, tab)` und Test.
|
||||
- `ref` ist `ticket.woid || ticket.$id`. Reine woid-Aufrufer übergeben die woid.
|
||||
- Rückgabe ist `/tickets/` + `encodeURIComponent(ref)`, bei `arbeitsblaetter` mit `/arbeitsblaetter`.
|
||||
- Ein leeres `ref` ergibt `/tickets`.
|
||||
- Die angezeigte Kurznummer `$id.slice(-5)` ist nie Linkziel.
|
||||
- Bestehende Links auf `/tickets?woid=N` gibt es heute an sechs Stellen. Alle nutzen künftig `ticketPath`:
|
||||
- `PlanboardPage`
|
||||
- `AssignProjectPage` (2×)
|
||||
- `ServicesPage`
|
||||
- `ProjectContextPanel`
|
||||
- `CustomerFunktionen`
|
||||
- Neue Links:
|
||||
- Zeilen der Ticketliste (P2)
|
||||
- Tickets-Reiter der Kundenseite (P4, heute ohne Link)
|
||||
- Ticket-Spalte der Projekte (P5, `project.ticketId` ist eine `$id`)
|
||||
- Alte Links `/tickets?woid=N` leiten mit `replace` auf `/tickets/N` um.
|
||||
- Das bleibt dauerhaft, auch nach P10, weil Portal-Mails so verlinken.
|
||||
- Die Umleitung liegt innerhalb der geschützten Route, damit `safeReturnPath` samt Suchteil nach dem Login greift.
|
||||
|
||||
### Laden
|
||||
|
||||
Neuer Hook `useWorkorder(ref)`:
|
||||
1. Zuerst sucht er per `Query.equal('woid', [String(ref)])`, `Query.orderDesc('$createdAt')`, `Query.limit(1)`.
|
||||
2. Findet er nichts, holt er das Ticket per `getDocument(ref)` als `$id`.
|
||||
3. 400 und 404 gelten beide als „Ticket nicht gefunden“.
|
||||
|
||||
Der Hook liefert dasselbe Schreib-API wie `useWorkorders` (`updateWorkorder`). Er kennt die Zustände Laden, Fehler und „Ticket nicht gefunden“.
|
||||
|
||||
Arbeitsblätter und `CreateWorksheetModal` nutzen immer `ticket.woid` des geladenen Tickets, nie den URL-Parameter. Für Tickets ohne woid zeigt der Reiter Arbeitsblätter den Hinweis „Für dieses Ticket gibt es keine Ticketnummer, Arbeitsblätter sind nicht möglich.“ statt eines Fehlers. `worksheets.woid` ist Pflicht.
|
||||
|
||||
### Seite `pages/TicketPage.jsx`
|
||||
|
||||
- **Kopf:**
|
||||
- Brotkrumen „Tickets / Nummer“. Der Link „Tickets“ führt mit dem Suchteil zurück, den die Liste per Link-State `listSearch` mitgibt. Ohne State führt er auf `/tickets`.
|
||||
- Eyebrow „Nummer · Art“. Die Art kommt aus `ticketTypeLabel`, „Webpage“ bleibt „Webpage“.
|
||||
- h1 mit dem Betreff (Rückfall „Kein Betreff“), `.sub` mit dem Kunden als Link und dem Ort.
|
||||
- Aktionen: Dringend-Schalter, Status-Auswahl und der schwarze Knopf „Arbeitsblatt hinzufügen“. Der Knopf erscheint für **alle** Ticketarten, auch Akquise, wie heute. Der Prototyp blendet ihn bei Akquise aus, das wird nicht übernommen.
|
||||
- **`.facts`:**
|
||||
- Zuständig als Auswahl
|
||||
- Termin: `startDate` mit `startTime`–`endTime`, ohne Rückfall auf das Erstelldatum, leer „Kein Termin“
|
||||
- Fällig: `deadline`, leer „–“
|
||||
- Einsatz: `serviceTypeLabel`
|
||||
- Angefragt von, leer „–“
|
||||
- Erstellt: `formatDistanceToNow`, deutsch, wie heute
|
||||
- **Datumsfelder sind Freitext.**
|
||||
- Angezeigt wird über `parseGermanDate` und `formatTime` aus `lib/planboard.js`.
|
||||
- Ist ein Wert nicht lesbar, steht der Rohtext da.
|
||||
- Nie wird `new Date()` oder `format()` direkt auf den gespeicherten Text angewendet.
|
||||
- **Reiter:** „Details“ und „Arbeitsblätter (n)“.
|
||||
- Details zeigt die Beschreibung als `.prose`, den Projekt-Abschnitt (`TicketProjects`, optisch bis P5 unverändert) und „Weitere Tickets von {Kunde}“ (`ProjectContextPanel`, Sichtbarkeitsregel wie heute).
|
||||
- Arbeitsblätter zeigt `.stats` und die Arbeitsblatt-Zeilen.
|
||||
- **Status-Auswahl** (natives `<select>`):
|
||||
- Sie bietet die sechs Werte aus `TICKET_STATUSES` an, für Akquise ebenfalls.
|
||||
- Ist der aktuelle Wert nicht dabei (Akquise- oder Altwert), steht er zusätzlich als gewählte Option darin.
|
||||
- Geschrieben wird nur `{ status }`.
|
||||
- **Zuständig** (natives `<select>`):
|
||||
- Optionen: „Nicht zugewiesen“ und die Mitarbeiter mit vollem Namen.
|
||||
- Ist `assignedTo` nicht in der Mitarbeiterliste, steht zusätzlich eine gewählte Option mit `assignedName`, sonst „Unbekannte Person“.
|
||||
- Solange die Liste lädt, ist die Auswahl deaktiviert.
|
||||
- Geschrieben wird nur `{ assignedTo }`, für niemanden ein leerer String.
|
||||
- `useEmployees` läuft einmal pro Seite.
|
||||
- **`TicketCard.jsx` entfällt.**
|
||||
- Vorher wandert die ungetestete Logik aus `handleCreateWorksheet` in eine Lib-Funktion mit Test: Status zurückschreiben und Akquise-Ergebnis setzen.
|
||||
- Testfälle: Akquise + Zugesagt → Kunde fest, Akquise + Abgesagt → verloren, Akquise + In Kontakt → keine Änderung, Nicht-Akquise → nie eine Änderung.
|
||||
- `useWorksheets(ticket.woid)` und `useCustomerAbo` laufen einmal pro Seite.
|
||||
- `aboHoursAllowed`, `onTogglePlan`, `canEditWorksheetTimes`, `canAddOwnSupport` und die drei Dialoge werden unverändert verdrahtet.
|
||||
|
||||
### Ticketliste `TicketsPage`
|
||||
|
||||
- **Reiter mit Zählern:** Aktiv · Offen · In Arbeit · Wartet auf Kunde · Prüfung · Erledigt · Storniert · Alle.
|
||||
- Aktiv = `DEFAULT_STATUS_GROUPS` (offen, in Arbeit, wartend, Prüfung).
|
||||
- Die übrigen Reiter entsprechen je einer Gruppe aus `STATUS_GROUPS`.
|
||||
- Alle bedeutet: kein Statusfilter.
|
||||
- „Storniert“ ist neu gegenüber dem Prototyp, damit die heutige Auswahl „nur stornierte“ nicht verloren geht.
|
||||
- Startzustand: Reiter Aktiv, Art Aufträge, „Nur dringende“ aus.
|
||||
- **Art-Auswahl** (`.select-quiet`) mit allen sechs heutigen `TYPE_FILTERS`: Aufträge (Standard, ohne Akquise) · Webpage · Migration · Projekt · Akquise · „Aufträge und Akquise“ (heute „Alle“).
|
||||
- **Kästchen** „Nur dringende“.
|
||||
- **Suche:**
|
||||
- Sie läuft im Browser über `ticketMatches`, wie heute.
|
||||
- Sie startet 300 ms nach der letzten Eingabe oder per Enter.
|
||||
- Der Knopf „Anwenden“ entfällt.
|
||||
- **Veraltete Antworten:** `useWorkorders` darf als einzige Hook-Änderung veraltete Antworten verwerfen (Anfrage-Zähler). So kann eine ältere Antwort nie eine neuere überschreiben.
|
||||
- **URL-Zustand** (`useSearchParams`):
|
||||
- Reiter, Art, Dringend, Suche und `limit` (Anzahl geladener Tickets, keine Seiten) stehen in der URL.
|
||||
- Geschrieben wird immer mit `replace`, Standardwerte werden nicht geschrieben.
|
||||
- Ungültige Werte fallen auf den Standard zurück. `limit` wird auf 10 bis 500 begrenzt.
|
||||
- Jede Filteränderung setzt `limit` auf 10 zurück.
|
||||
- `?woid=` hat Vorrang und leitet um.
|
||||
- Die Helfer stehen in neuer `lib/ticketListUrl.js` mit Test.
|
||||
- **Nachladen:**
|
||||
- Beim Nachladen bleiben die alten Zeilen sichtbar, im Fuß steht dezent „Wird geladen …“.
|
||||
- Den leeren Ladezustand gibt es nur beim allerersten Laden.
|
||||
- „Mehr laden“ bleibt (+10), ohne Suche wie heute.
|
||||
- **Zähler:**
|
||||
- Je Reiter läuft eine Abfrage mit `Query.limit(1)`, die `total` liest. Sie nutzt dieselbe Art und denselben Dringend-Filter wie die Liste, nie den Suchtext.
|
||||
- Die Zähler laden beim Öffnen, bei Wechsel von Art oder Dringend und nach dem Anlegen eines Tickets, nicht bei Reiterwechsel oder Tastendruck.
|
||||
- Bei aktiver Suche werden sie ausgeblendet, der Fuß zeigt dann die Trefferzahl.
|
||||
- Schlägt eine Abfrage fehl, fehlt nur der Zähler.
|
||||
- Eigener Hook, `useWorkorders` bleibt dafür unberührt.
|
||||
- **Tabellenspalten:**
|
||||
- **Ticket:** Nummer, Betreff, Badge „Dringend“, darunter Kunde · Ort.
|
||||
- **Status.**
|
||||
- **Zuständig:** „Vorname N.“ aus der Mitarbeiterliste, Rückfall `assignedName`, sonst „Unbekannt“. Ohne Zuweisung steht „Niemand“ in Grau.
|
||||
- **Termin:** wie in den Facts, Fehlwert „–“.
|
||||
- **Zeit:** siehe unten.
|
||||
- **Erstellt.**
|
||||
- **Spalte „Zeit“:**
|
||||
- Wert ist die Summe von `worksheetMinutes` über alle Arbeitsblätter ohne Kommentar. Das ist dieselbe Regel wie „Arbeitszeit gesamt“ auf der Ticketseite. Formatiert wird mit `stepLabel`.
|
||||
- Geladen wird für die geladenen Zeilen in Blöcken von höchstens 100 woids: `Query.equal('woid', block)`, `Query.select([...])`, `Query.limit(5000)`, höchstens 3 Blöcke gleichzeitig.
|
||||
- Das Ergebnis wird je woid zwischengespeichert, „Mehr laden“ holt nur neue Zeilen.
|
||||
- Die Zeit wird höchstens für die ersten 200 Zeilen berechnet. Sind mehr geladen, nennt der Fuß das.
|
||||
- Ein Block mit 5000 Treffern könnte abgeschnitten sein. Er und ein Fehler zeigen „–“, nie eine falsche Zahl. Die Liste bleibt bedienbar.
|
||||
- Tickets ohne woid zeigen „–“.
|
||||
- Die Summenfunktion bekommt einen Test.
|
||||
- **Sortierung:** Sie bleibt `$createdAt` absteigend, der Fuß sagt „Neueste zuerst“.
|
||||
- **Navigation:**
|
||||
- Die Zeilen verlinken mit `state={{ listSearch: location.search }}`.
|
||||
- Nach „Neues Ticket“ springt die App auf die neue Ticket-Seite (`createWorkorder` liefert die woid).
|
||||
|
||||
### Arbeitsblätter (`WorksheetList` neu)
|
||||
|
||||
- **Aufbau:** flache Zeilen `.ws` in drei Bereichen:
|
||||
- links Datum und Uhrzeit
|
||||
- in der Mitte Person, Einsatz (bei Kommentaren das Wort „Kommentar“), Nummer, Badges, Text, Unterstützung, „Status danach: X“ und Aktionen
|
||||
- rechts die Zeit
|
||||
- Es gibt kein Aufklappen, keine Verläufe und keine Animation.
|
||||
- Kommentare stehen in Nebentextfarbe.
|
||||
- **Git-Push:** ohne Zeit Badge „Zeit offen“ (orange), mit Zeit „Zeit eingetragen“ (grün).
|
||||
- **Abo:** Der Umschalter heißt „Abo“ beziehungsweise „+ Abo“. Seine Sichtbarkeitsregel bleibt exakt: `onTogglePlan && !isComment && totalTime > 0 && (aboHoursAllowed || countsToPlan)`.
|
||||
- **Bedingungen, die bleiben:** `canEditTimes`, `canAddSupport`, `isGit` und `needsTimeEntry`.
|
||||
- **Texte, die bleiben:** „Zeit nachtragen“, „Zeiten bearbeiten“, „Ich habe unterstützt“ und „Meine Unterstützung bearbeiten“. Letzteres erscheint, sobald man schon unterstützt. `OwnSupportModal` behält „Eintrag entfernen“ und `maxLength` 200.
|
||||
- **Reihenfolge:** neueste zuerst.
|
||||
- **Zeitformat:** überall `stepLabel` („1 h 30 min“, heute „1h 30min“).
|
||||
- **Kennzahlen:**
|
||||
- Arbeitszeit gesamt = Summe `worksheetMinutes` ohne Kommentare
|
||||
- davon Abo-Stunden = dieselbe Summe über `countsToPlan && !isComment`
|
||||
- Zeiten offen = Anzahl `needsTimeEntry`, orange bei mehr als 0
|
||||
|
||||
## Bewusste Änderungen am Verhalten
|
||||
|
||||
| Bisher | Künftig |
|
||||
|---|---|
|
||||
| Ticket klappt in der Liste auf | eigene Seite mit Link |
|
||||
| Statusfilter als Mehrfachauswahl mit „Anwenden“ | Reiter (Einfachwahl) |
|
||||
| Suche nach Klick auf „Anwenden“ | Suche nach Tipp-Pause |
|
||||
| Footer auf jeder Seite | Footer entfällt, Text steht im Login-Fuß |
|
||||
| Menü einklappbar | nicht mehr einklappbar, auf dem Handy eine Schublade |
|
||||
| Dialoge schließen nur mit × | Dialoge schließen mit × und neu mit Esc |
|
||||
| Statusfarben: Offen grün, Erledigt grau | Farben wie in der Tabelle oben. Das Team wird beim Live-Gang von P1 informiert. |
|
||||
| Kein Link auf die Tickets einer Kundenseite oder in der Projektliste | beide verlinkt |
|
||||
|
||||
Alle anderen Funktionen bleiben erhalten.
|
||||
|
||||
## Pakete
|
||||
|
||||
### Arbeitsweise
|
||||
|
||||
- **Branch:** Gearbeitet wird im Worktree `dev-design` auf `feature/design-weiss`, abgezweigt von `test` 603d08c.
|
||||
- **Commits:** Jedes Paket ist ein eigener Commit-Stapel. `git add` nur mit ausdrücklichen Pfaden.
|
||||
- **Rebase:** Vor jedem Paket und unmittelbar vor jeder Abnahme läuft `git fetch origin && git rebase origin/test`, kein Merge.
|
||||
- **Tag:** Erst danach wird das Tag `design-p<N>` auf den abgenommenen Stand gesetzt und nicht mehr bewegt. Korrekturen nach dem Push sind Folge-Commits mit eigenem Tag `design-p<N>-fix<k>`.
|
||||
- **Live-Gang:** Live geht ein Paket nur über das Tag, nie über die Branch-Spitze. Der Eigentümer führt aus:
|
||||
`git -C /home/webklar/apps/ticket.webklar.com/dev-design push origin 'design-p<N>^{commit}:refs/heads/test'`
|
||||
Das ist ein reiner Fast-Forward. Seit 2026-09-30 deployt nur `test`. Vor dem Push nennt der Agent die genaue Zeile, den SHA und `git log --oneline origin/test..design-p<N>`.
|
||||
- **Nach dem Push:**
|
||||
- Der Agent prüft im Log von `webklar-ticket-build` die Zeile „Deployment completed successfully“.
|
||||
- Er vergleicht den Asset-Hash von `https://ticket.webklar.com/` mit `dist/index.html`.
|
||||
- Er macht einen kurzen Sichttest im Chrome des Eigentümers, nur lesend.
|
||||
- **Zurücknehmen:** Per Revert-Commit, immer das jüngste Paket. Ein früheres Paket geht nur zusammen mit allen späteren: `git revert --no-commit design-p<N-1>..design-p<N>`.
|
||||
- **Reihenfolge:**
|
||||
- Live gehen die Pakete in der Reihenfolge P1 bis P10.
|
||||
- P3 bis P5 brauchen `ticketPath` aus P2.
|
||||
- P4 liegt vor P5, weil P4 den Aufruf von `CreateCustomerForm` in `AssignProjectPage` umstellt.
|
||||
- P7, P8 und P9 hängen fachlich nicht voneinander ab, gehen aber auf demselben Branch in dieser Reihenfolge live.
|
||||
- **Parallelarbeit:** Am nächsten Paket wird erst nach dem Tag des vorigen gearbeitet. Die Commits liegen hinter dem Tag und gehen deshalb nicht mit live.
|
||||
|
||||
### Inhalt
|
||||
|
||||
| Paket | Umfang | Danach sichtbar |
|
||||
|---|---|---|
|
||||
| **P0** Vorbereitung | `LeadsPage.jsx` und `hooks/useLeads.js` löschen. Beide sind toter Code. Die `/leads`-Umleitung, der pausierte Menüpunkt und `lib/leads.js` bleiben. `node_modules` wird aus Git genommen (`git rm -r --cached node_modules`; `.gitignore` führt es schon). Der Deploy installiert ohnehin neu. Im Worktree folgt ein echtes `npm ci` unter `systemd-run` mit Speichergrenze, ohne Symlink. Danach muss `git status --short` ohne `??`-Zeilen leer sein. Das Screenshot-Werkzeug entsteht außerhalb des Repos (siehe Abnahme), und es werden Vorher-Bilder erzeugt. Fertig, wenn `npm test` und `npm run build` im Worktree grün laufen und ein Befehl alle Vorher-Bilder erzeugt. | nichts |
|
||||
| **P1** Fundament | Tokens, Aliase und KOMPAT, Basis, Gerüst, Menü, Logo, Favicon. Alle `ui/*`-Bausteine samt `Dialog` und `Stat`, außerdem `statusTone` und der neue Login. Löschen: Lato, `Footer.jsx`, `LetterGlitch.jsx`, `lib/sidebar.js` samt Test und tote CSS-Regeln laut Skript-Liste. Pflicht-Fixes gegen Unlesbares: Chat-Variablen in `chat.css` unter `.woms-chat` (`--chat-on-accent: #fff`, `--chat-line: var(--line-strong)`, `--chat-muted: var(--text-2)`, Fehler- und Hinweistexte `--red-fg`/`--text-2`). `.admin-primary-button` bekommt weiße Schrift, Hover `--accent-hover`. Farbliterale umstellen in `AssignedProjectCard`, `PreviewBlock`, `PreviewLinkButton`, `GiteaLinkButton`, `AnalyseRow`, `CreateWorksheetModal` und `WorksheetTimesModal` (`#a0aec0`) sowie die helle Warn- und Fehlerschrift in `admin-areas.css`, `serverpuls.css`, `AdminPage.css` und im Finanz- und Rechtlich-Teil von `global.css`. `chartColors.js` wird angelegt und in `Chart.jsx` und `ServerpulsPage` benutzt. Abschluss-Prüfung: `rg` über alle Farbliterale in `src`. Jedes Literal mit Kontrast unter 3:1 auf Weiß, jede dunkle Schrift auf `--accent` und jedes Weiß-Alpha ist entweder behoben oder im Plan mit Zielpaket als bewusst offen eingetragen. | ganze App weiß, Menü als Text, neues Login, alle Dialoge weiß; Innenlayouts noch alt, aber lesbar |
|
||||
| **P2** Tickets | Ticketliste, Ticket-Seite, Route, `ticketRoutes.js`, `ticketListUrl.js` und Arbeitsblätter. Dialoge `CreateTicketModal`, `CreateWorksheetModal`, `WorksheetTimesModal`, `OwnSupportModal`; `SupportersEditor` als Feldblock in den Arbeitsblatt-Dialogen. Native Auswahlfelder statt `StatusDropdown`/`EditorDropdown`, dazu der Dringend-Schalter und „Weitere Tickets“. Die sechs Link-Stellen wechseln auf `ticketPath`. Altklassen `.tcard*`, `.dropdown*`, `.ticket-tab*`, `.worksheet-summary` und `.chevron` löschen. „Neues Ticket“ bleibt inhaltlich wie heute, also automatische Zuweisung und kein Einsatzfeld. | Ticket-Arbeitsplatz wie im Prototyp |
|
||||
| **P3** Planboard | `.week` mit sieben Spalten, Haarlinien und Wochenende `--bg-subtle`. Heute als schwarze Datumspille, Termine als Links auf `ticketPath`. Akquise-Termine hell orange, stornierte bleiben sichtbar wie heute. Kopf: „Kalenderwoche N · Datum bis Datum · n Termine“, Aktionen ← · Heute · →. Unter 860 px eine Spalte. | Planboard ohne Karten |
|
||||
| **P4** Kunden | Kundenliste mit Reitern Feste Kunden · Leads · Archiviert · Alle. Die Zähler nutzen dieselbe Filterfunktion wie die Liste (`lib/leads.js`). Dazu Suche und Tabelle mit Unternehmen und Ansprechpartner, Kundennummer, Standort, E-Mail, Telefon. Badges „Lead“, „Abgesagt“, „Archiviert“ neben dem Namen wie heute; „Wichtig“ als Badge statt Stern. Kopf: für Admins „Neuer Kunde“ schwarz und „Neuer Lead“ als Standardknopf, für andere nur „Neuer Lead“ als Standardknopf. `CreateCustomerForm` und `CreateLeadForm` werden Dialoge, Hinweis- und Knopftexte bleiben; der Aufruf in `AssignProjectPage` wird mit umgestellt. Kundenseite mit Brotkrumen, Eyebrow und Reitern; ihr Tickets-Reiter verlinkt die Tickets. Übersicht als `.two-col` mit `.kv`, Stammdaten bleiben direkt bearbeitbar. `CustomerFunktionen` als Tabelle, die weiter `abgeschaltetText` zeigt (kommt aus tp2, siehe Risiken). Außerdem `AboStatusCard`, `CustomerViewLog`, `KundenansichtButton` (Standardknopf) und Grundstil von `admin-areas.css`. | Kundenbereich wie im Prototyp |
|
||||
| **P5** Projekte | `ProjectsPage` als eine Tabelle: Projekt + Repo, Kunde (Auswahl je Zeile wie heute, deaktiviert bei archiviert oder während der Zuordnung, Rückfrage beim Entfernen), Art, Adresse, Ticket als Link, Status, Zuletzt geändert = `$updatedAt`, Aktionen. Die Aktionen sind Textlinks, nur wenn zutreffend: „Vorschau öffnen“, „In Gitea öffnen“, „Hosten“ (mit Ladezustand), „Aktualisieren“. Die erste Zelle ist ein `<button class="link">` mit dem Projektnamen und öffnet „Projekt bearbeiten“. Auf dem Handy bleiben Projekt, Kunde, Status und Aktionen sichtbar. Die heutigen sechs Ansichten werden 1:1 Reiter: Aktive Projekte · Unzugeordnet · Zugeordnet · Vorlagen (nur Admins) · Archiviert · Alle inkl. Archiv. Dazu Suche, Auswahl Kunde und Mitarbeiter, „Weitere Treffer“ und Hinweiszeile. „Neues Projekt“ als Dialog mit Repo-Name. `PreviewBlock` steht außerhalb des Formulars, der Dialog ist `wide`. Die Liste zeigt „Wird geladen …“ nur beim ersten Laden. Außerdem: `AssignProjectPage` mit Schritten als Abschnitten und Auswahl auch als Text markiert; `ServicesPage`; `TicketProjects`; `AssignedProjectCard`; `AnalyseRow` (Zustimmung als Dialog, Wortlaut unverändert); `PreviewBlock`; `PreviewLinkButton` („Vorschau öffnen“); `GiteaLinkButton` („In Gitea öffnen“); `CopyableCredential` („Anzeigen“/„Verbergen“, „Kopieren“/„Kopiert“). | Projektbereich wie im Prototyp |
|
||||
| **P6** Kunden-Unterseiten | Chat: Liste als Haarlinien-Zeilen. Alle Regeln, die Elemente im Verlauf und in der Eingabezeile gestalten, tragen das Präfix `.woms-chat`, bei Bedarf `.woms-chat.woms-chat`, nie `!important`. Das Seitenlayout (`.chat-page`, `.chat-side`, `.chat-list*`, `.chat-main`, `.chat-back`) bleibt ohne Präfix. Fremde Blase `--bg-hover`, eigene `--grey-bg`. `loadChatComposer.js`, sein Test und die Container in `ChatThread.jsx` bleiben unverändert. Gutschriften: Filter als Reiter, Tabelle mit Spaltenpriorität, Detail als `.two-col`, Knöpfe Genehmigen (schwarz), Rückfrage (Standard), Ablehnen (rot als Text). Abo-Angebote: Tabelle, Formular als Dialog. E-Mail-Speicher: Tabelle mit `data-storage-done`. | Chat und Kunden-Unterseiten hell |
|
||||
| **P7** Finanzen | Finanz-CSS neu (ca. 320 Zeilen), alle Finanzkomponenten. `LiquidityHead` wechselt von `Kpi` auf `Stat`, der `Kpi`-Export wird gelöscht. Dialoge `AssignDialog` und `InvoiceCreateModal` (`wide`). Bleiben: die Anker `INVOICES_ANCHOR` und `finanzen-buchungen` samt `scroll-margin-top`, die Klassen-Hooks, `data-label`, `role`-Attribute und Container-Queries; `container-type` wandert auf die Wrapper. Beträge behalten ihr Vorzeichen, negative sind rot. | Finanzen ohne Karten |
|
||||
| **P8** Rechtliches, Admin | `LegalPage`, `LegalTextsTab`, `ProviderContractsTab` als Tabelle, `ContractForm`/`VersionForm` als Dialoge. `AdminPage` mit Tabelle, Formular als Dialog und Textaktionen „Bearbeiten“/„Löschen“. Fehler erscheinen als `.msg.err` im Dialog, der erst bei Erfolg schließt. Die einmal angezeigten Zugangsdaten bleiben ein Abschnitt auf der Seite. `AdminPage.css` wird neu geschrieben; `.credential*`, `.sr-only` und `.spin` ziehen nach `global.css` um. | Rechtliches und Admin ohne Karten |
|
||||
| **P9** Serverpuls | Kopf mit Text-Badge statt Emoji. Warnungen als Listenzeilen mit Stufen-Badge, Kacheln als `.stats` mit Sparkline. Deltas als Textfarbe (rot = gestiegen, grün = gesunken), Umschalter `.seg`. Alle `data-testid` bleiben. | Serverpuls hell |
|
||||
| **P10** Aufräumen | `react-icons` und `@tabler/icons-react` deinstallieren, sobald `rg "react-icons\|@tabler" src` 0 Treffer liefert. KOMPAT-Block, Aliase und tote Klassen löschen. `previewEnv.dotColor` samt Test entfernen. Die Kommentare zu Icons in `navLinks.js` und im Test anpassen. Die Bundle-Größe vorher und nachher dokumentieren. Die `?woid=`-Umleitung bleibt. | nichts Neues, kleineres Bundle |
|
||||
|
||||
### Seiten ohne Prototyp-Vorlage
|
||||
|
||||
Gemeint sind P6 bis P9, `ServicesPage` und `AssignProjectPage`. Sie entstehen aus denselben Bausteinen nach festen Ersetzungen:
|
||||
- Karte → Abschnitt
|
||||
- Kachelraster → Tabelle oder Liste
|
||||
- farbiger Streifen → Badge
|
||||
- Icon-Knopf → Textknopf oder Textlink
|
||||
|
||||
Der Eigentümer nimmt diese Pakete anhand von Screenshots ab, bevor er pusht.
|
||||
|
||||
### Texte
|
||||
|
||||
Texte bleiben wörtlich erhalten. Ausnahmen sind nur diese neuen oder geänderten Texte:
|
||||
|
||||
**Login:** „Anmelden“, „E-Mail“, „Passwort“, „Weiter“, „Code eingeben“, „Code erneut senden“, „Abbrechen“ sowie der Untertitel „Ticketsystem für das WEBklar-Team“.
|
||||
|
||||
**Menü und Laden:** „Abmelden“, „Menü“, „Wird geladen …“ statt „Loading...“.
|
||||
|
||||
**Ticketliste:**
|
||||
- Reiter und Art-Eintrag „Aufträge und Akquise“
|
||||
- Fuß „Neueste zuerst“
|
||||
- „Niemand“, „Unbekannt“
|
||||
|
||||
**Ticketseite:**
|
||||
- „Nicht zugewiesen“ (statt „(Unassigned)“), „Unbekannte Person“
|
||||
- „Kein Termin“
|
||||
- „Weitere Tickets von {Kunde}“
|
||||
- „Status danach: X“
|
||||
- Hinweis für Tickets ohne Ticketnummer
|
||||
- Zeitformat `stepLabel`
|
||||
|
||||
**Weitere Seiten:**
|
||||
- Planboard-Kopf
|
||||
- Badge „Wichtig“
|
||||
- Spaltenköpfe der neuen Tabellen
|
||||
|
||||
**Knöpfe:** Textknöpfe, die bisher Icons waren:
|
||||
- „Vorschau öffnen“, „In Gitea öffnen“
|
||||
- „Bearbeiten“, „Löschen“, „Entfernen“, „Lösen“
|
||||
- „Anzeigen“, „Verbergen“, „Kopieren“, „Kopiert“
|
||||
- „Erneut versuchen“, „Aktualisieren“, „Hosten“
|
||||
|
||||
**Ladetexte in Knöpfen:** „Speichert …“, „Sendet …“, „Wird geladen …“.
|
||||
|
||||
## Was gleich bleibt
|
||||
|
||||
**Verhalten:**
|
||||
- Alle Hooks und alle Lib-Logik. Einzige Ausnahme: das Verwerfen veralteter Antworten in `useWorkorders`. Neue Hooks sind erlaubt (`useWorkorder`, Zähler, Zeitsummen).
|
||||
- Auth und 2FA.
|
||||
- Alle Abfrage-Intervalle.
|
||||
- Pflichtfelder und 15-Minuten-Schritte.
|
||||
- Doppelklick-Sperren.
|
||||
- Alle `window.confirm`-Rückfragen.
|
||||
- Alle `data-testid`.
|
||||
- Die URL-Parameter `?tab=`, `?antrag=`, `?kunde=`.
|
||||
|
||||
**Daten:**
|
||||
- Appwrite-Schema, Rechte und gespeicherte Werte (englische Status, die das Kundenportal auswertet).
|
||||
- Server-APIs.
|
||||
- Portal-Repo.
|
||||
- `.env*`: Latin-1, nicht anfassen.
|
||||
|
||||
**Texte:**
|
||||
- Alle rechtlich relevanten Texte:
|
||||
- Zustimmung zur Website-Analyse
|
||||
- Abo-, Gutschrift- und § 19-Hinweise
|
||||
- Steuer-Hinweis
|
||||
- Mail- und Chat-Vorlagen.
|
||||
- Fehlermeldungen.
|
||||
|
||||
## Risiken und Gegenmaßnahmen
|
||||
|
||||
| Risiko | Gegenmaßnahme |
|
||||
|---|---|
|
||||
| Ein einziges CSS-Bündel: umbenannte Klassen brechen Seiten, die gerade niemand ansieht | KOMPAT ab P1. Am Ende von P1 ein Skript, das alle in JSX und `lib/*.js` benutzten Klassen gegen die CSS-Regeln abgleicht; Ergebnis im Plan. Screenshot-Pflichtliste nach jedem Paket. |
|
||||
| Namensgleiche Klassen mit anderer Bedeutung: `.btn`, `.badge`, `.page`, `.field`, `.seg`, `.toolbar`, `.empty`, `.list-row`, `.urgent-toggle`, `.overlay`, `.id` | in P1 bewusst ersetzen, jede Altverwendung im Screenshot prüfen |
|
||||
| 79 `!important` im Altbestand | löschen statt überschreiben |
|
||||
| Alt-Dialoge bleiben bis P2/P5/P7/P8 in Betrieb | KOMPAT-Regeln für `.overlay`, `.overlay-content`, `.overlay-close`. P1-Abnahme: jedes der 10 Alt-Dialoge bei 390×700 und 1440×700, Speichern-Knopf erreichbar. |
|
||||
| Fremder Chat-Composer, sein CSS lädt nach dem WOMS-CSS | Präfix-Regel und `--chat-*`-Variablen, siehe P1 und P6 |
|
||||
| Diagramme auf Canvas | Farbkonstanten statt Variablen, ab P1 hell |
|
||||
| Statusfarben ändern ihre Bedeutung | Hinweis an das Team beim Live-Gang von P1 |
|
||||
| Dokument-Scroll statt innerem Scroller: `position: sticky` in AdminPage, Scroll-Anker der Finanzen | in der Abnahme gezielt prüfen |
|
||||
| Vorschlagslisten in scrollenden Dialogen werden abgeschnitten | in P2, P6 und P7 prüfen |
|
||||
| Freitext-Datumsfelder, keine Fehlergrenze: ein Parserfehler legt die App lahm | nur über `parseGermanDate`/`formatTime` anzeigen. Testdaten mit Altwerten: `2.5.2026`, `31.02.2026`, ISO-Datum, leer, ohne woid, unbekannter Status. |
|
||||
| Paralleler Branch `feature/terminplaner-tp2`: Commit 6f03712 ändert `CustomerFunktionen.jsx`, `lib/funktionenView.js` (neue Funktion `abgeschaltetText`) und den Test (+4 Tests) | vor P4 nach `test` bringen (Eigentümer) oder per `git cherry-pick 6f03712` in `dev-design` holen. Die Testzahl steigt dann um 4. |
|
||||
| `node_modules` aus Git nehmen (P0) | betrifft nur Git, nicht den Deploy, weil `deploy.sh` löscht und neu installiert. Andere Worktrees mit Symlink sind unkritisch. |
|
||||
| Speicher knapp | Installation, Build und Screenshots unter `systemd-run` mit Speichergrenze |
|
||||
|
||||
## Abnahme
|
||||
|
||||
Ein Paket ist fertig, wenn alles Folgende erfüllt ist:
|
||||
|
||||
1. **Tests:** `npm test` läuft im Worktree grün. Die Testzahl wird je Paket notiert. Ausgangsstand 312; P1 minus 3 (`sidebar`) plus neue Tests; nach tp2 plus 4. Neue Tests gibt es für `statusTone`, `ticketPath`, `ticketListUrl`, die ausgelagerte Arbeitsblatt-Logik und die Zeitsummen.
|
||||
2. **Build:** `npm run build` ist grün, die Bundle-Größe wird notiert.
|
||||
3. **Screenshots** mit 1440 px und 390 px über die Pflichtliste:
|
||||
- Login inklusive beider Code-Schritte
|
||||
- Ticketliste (mit und ohne Suche), Ticket mit beiden Reitern, alle Ticket-Dialoge
|
||||
- Planboard
|
||||
- Kundenliste und Kundenseite mit allen Reitern
|
||||
- Chat
|
||||
- Gutschriften, Abo-Angebote, E-Mail-Speicher
|
||||
- Projekte mit beiden Dialogen, Zuordnen, Services
|
||||
- Zustimmung der Website-Analyse
|
||||
- Finanzen mit Dialogen
|
||||
- Rechtliches
|
||||
- Admin inklusive „Mitarbeiter anlegen“
|
||||
- Serverpuls mit Diagrammen
|
||||
- in P1 zusätzlich die 10 Alt-Dialoge
|
||||
4. **Screenshot-Werkzeug:**
|
||||
- Es liegt unter `/root/reports/ticketsystem-design-2026-10-04/werkzeug/`, außerhalb des Repos, und ändert weder `package.json` noch Lock noch `tests/`.
|
||||
- Aufbau: esbuild-Bündel und Playwright Headless mit nachgebildetem Appwrite. Es nutzt nur Testdaten, inklusive der Altwerte aus der Risikotabelle, keine Produktionsdaten und keine echten Logins.
|
||||
- Es läuft unter `systemd-run` mit `MemoryMax`.
|
||||
- Die Ausgabe liegt je Paket unter `shots/p<N>/1440` und `shots/p<N>/390`.
|
||||
5. **Code-Hygiene im umgebauten Bereich:**
|
||||
- keine Icon-Importe
|
||||
- keine neuen `style={{…}}`, außer für Werte, die erst zur Laufzeit feststehen (z. B. berechnete Breiten)
|
||||
- Texte nur wie unter „Texte“ geändert
|
||||
6. **Code-Review:** Ein Review (sonnet) ist gelaufen, wichtige Befunde sind behoben.
|
||||
7. **Freigabe:** Der Eigentümer hat die Screenshots gesehen, bevor er das Tag pusht.
|
||||
@@ -0,0 +1,694 @@
|
||||
> Anhang zu `2026-10-04-weisses-design-design.md`: Ergebnis der Bestandsaufnahme vom 2026-10-04 (6 Leser + Synthese, nur lesend). Bei Widersprüchen gilt das Konzept. Entschieden sind inzwischen: Akzent Schwarz, Ticket als eigene Seite (Abschnitt 6) und alle Standards aus Abschnitt 8.1, außer wo das Konzept etwas anderes festlegt (Tab-Zähler und Spalte "Zeit" werden mitgeliefert, Planboard zeigt Stornierte, Kundenliste ohne "Offene Tickets", Projekte-Spalte "Zuletzt geändert" = `$updatedAt`).
|
||||
|
||||
# Konvertierungskarte WOMS: dunkel zu weiß (Prototyp, Akzent Schwarz #111)
|
||||
|
||||
Stand 2026-10-04. Repo `/home/webklar/apps/ticket.webklar.com/build` (Branch `test` = `603d08c` = `origin/test`).
|
||||
Quellen sind der Prototyp (`/root/reports/ticketsystem-design-2026-10-04/quelle/styles.css`, 545 Zeilen, vollständig gelesen, und `app.js`), das Leser-Inventar (104 Dateien) und eigene Stichproben. Was ich selbst nachgeprüft habe, steht unter 0 und ist im Text mit [geprüft] markiert. Der Eigentümer hat Schwarz bestätigt ("schwarz passt"). Es gibt keine Akzent-Varianten (`html[data-accent]`) und keine `.proto`-Leiste.
|
||||
|
||||
---
|
||||
|
||||
## 0. Befunde aus der Gegenprüfung, die die Planung ändern
|
||||
|
||||
1. **Der Deploy-Webhook deployt nur `test`.** Die Aufgabenstellung und die Projektnotiz `woms-ticketsystem.md` gehen noch von "jeder gepushte Branch geht live" aus [geprüft].
|
||||
- `/home/webklar/apps/ticket.webklar.com/webhook-server.js` hat seit 2026-09-30 12:51:29 UTC `DEPLOY_BRANCH = "test"` ("ignoring push to X (only test deploys)").
|
||||
- Der Container `webklar-ticket-build` startete 12:51:56 UTC und mountet die Datei `:ro`, läuft also mit der neuen Version.
|
||||
- In den Logs der letzten 72 h stehen nur Pushes auf `test`. Ein Push auf einen Feature-Branch wurde nie getestet.
|
||||
- Empfehlung: Feature-Branches weiter nicht pushen, bis ein Probe-Push den Lock bestätigt hat. Der Eigentümer bringt Pakete live mit `git push origin <branch>:test`.
|
||||
2. **`build/` ist das Live-Checkout.** Es ist im Container als `/app` eingehängt [geprüft].
|
||||
- `deploy.sh` macht dort `git reset --hard origin/test`, `git clean -fd`, `rm -rf node_modules` und `npm install`.
|
||||
- Dort wird nie entwickelt. Entwicklung läuft in einem Worktree, zum Beispiel `dev-design` auf `feature/design-weiss` (von `test` abgezweigt), wie die vorhandenen `dev-*`-Worktrees.
|
||||
- Es gibt kein Staging. Jeder Push auf `test` ist Produktion, ein Poll alle 15 Minuten baut zusätzlich neue Commits auf `test`.
|
||||
3. **`feature/terminplaner-tp2` ist divergiert, nicht nur "1 voraus"** [geprüft].
|
||||
- Der Branch liegt 1 Commit voraus und 3 hinter `test`. Alle anderen `dev-*`-Branches sind vollständig in `test` enthalten (ahead = 0).
|
||||
- Der eine Commit `6f03712` ("Funktionen-Reiter: Abschaltgrund und Löschdatum") ändert `src/components/customer/CustomerFunktionen.jsx` (3 Zeilen), `src/lib/funktionenView.js` (+21) und `tests/unit/funktionenView.test.mjs` (+36).
|
||||
- Konfliktpotenzial hat nur die 3-Zeilen-Änderung in `CustomerFunktionen.jsx`, und die wird in Paket 4 neu gebaut. Siehe Abschnitt 7.
|
||||
4. **Ticket als eigene Seite ist laut Projektnotiz `woms-theme-weiss.md` schon entschieden.** Dort steht: "User decided: a ticket opens as its OWN PAGE (route per ticket, tabs Details/Arbeitsblätter), not inline expand." Die Aufgabe nennt die Frage noch offen. Abschnitt 6 begründet die Empfehlung unabhängig davon. Falls die Entscheidung nur mündlich fiel, bitte kurz bestätigen lassen.
|
||||
5. **Latin-1 gibt es nur in `.env.development` und `.env.production`** (ISO-8859, getrackt, nur Kommentarzeilen mit "Öffentliche … für") [geprüft]. Alle 93 JSX- und 65 JS-Dateien, Tests, CSS und `index.html` sind UTF-8 ohne BOM und ohne CRLF. Die `.env*` bleiben unangetastet.
|
||||
6. **`node_modules` ist teilweise in Git getrackt**: 8.890 von 9.117 getrackten Dateien (`date-fns` 5.722, `caniuse-lite` 837, `react-icons` 147, `@tabler` 0) [geprüft].
|
||||
- Für den Build ist das egal, weil `deploy.sh` `node_modules` löscht und neu installiert.
|
||||
- Praktische Folge: In Worktrees nur mit expliziten Pfaden `git add` und nie `git add -A`. Ein `npm uninstall` in einem Worktree zeigt sonst 147 gelöschte getrackte Dateien.
|
||||
7. **Baseline:** `npm test` = 312 Tests, 312 grün, 0,8 s [geprüft]. Bundle heute: JS 680.818 B, CSS 64.433 B, Schrift `lato-regular.ttf` 96.184 B.
|
||||
8. **`LeadsPage.jsx` ist toter Code** [geprüft]. Sie wird nirgends importiert, `/leads` leitet auf `/tickets` um, und `hooks/useLeads.js` wird nur von ihr benutzt. `lib/leads.js` dagegen lebt (Kunden-Seiten) und bleibt.
|
||||
9. **Das Test-Rezept für UI** steht in `woms-ticketsystem.md`. Vite dev wird per OOM getötet. Stattdessen gilt: esbuild-Harness unter `systemd-run` mit `MemoryMax=400M`, dann Playwright-Headless mit gemocktem Appwrite. Die Skripte sind weg und müssen in Paket 0 neu gebaut werden.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ausgangslage in Zahlen
|
||||
|
||||
### 1.1 JSX nach Bereich (eigene Zählung, `style={`, Farbliterale `#rrggbb`/`#rgb`/`rgb(a)(`, Icon-Tags `<Fa…/Icon…/Si…>`)
|
||||
|
||||
| Bereich | JSX-Dateien | Zeilen | Inline-`style` | Farbliterale | Icon-Tags | Dateien mit Icon-Import |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 Grundgerüst/UI/Login (App, Navbar, Footer, AdminRoute, LoginPage, LetterGlitch, `ui/*` 12) | 19 | 1.086 | 39 | 17 | 17 | 1 |
|
||||
| 2 Tickets, Arbeitsblätter, Projekt-Block, Planboard | 20 | 2.766 | 160 | 82 | 42 | 16 |
|
||||
| 3 Kunden, Chat, Gutschriften, Angebote, Speicher, Leads | 18 | 2.524 | 107 | 14 | 68 | 15 |
|
||||
| 4 Projekte, Zuordnen, Services, AnalyseRow | 4 | 1.419 | 95 | 4 | 25 | 3 |
|
||||
| 5 Admin, Finanzen, Rechtliches, Serverpuls | 31 | 3.726 | 7 | 14 | 35 | 16 |
|
||||
| AuthContext | 1 | 272 | 0 | 0 | 0 | 0 |
|
||||
| **Summe** | **93** | **11.793** | **408** | **131** | **187** | **51** |
|
||||
| davon LeadsPage (wird gelöscht) | 1 | 614 | 53 | 14 | 25 | 1 |
|
||||
| **Summe ohne LeadsPage** | **92** | **11.179** | **355** | **117** | **162** | **50** |
|
||||
|
||||
Die größten Inline-Style-Träger sind `ProjectsPage` (37), `WorksheetList` (33), `AssignProjectPage` (31), `PreviewBlock` (30), `LoginPage` (19), `AssignedProjectCard` (18), `AnalyseRow` (16), `TicketsPage` und `CustomersPage` (je 13) und `PlanboardPage` (12). Fast alles ist Layout (flex, gap, margin, fontSize). Echte Farben stehen nur in `WorksheetList` (Gradienten, Weiß-Alphas, `#9d3dfe`), `AssignedProjectCard`, `PlanboardPage`, `PreviewBlock`, `LoginPage`, den Link-Buttons und `LeadsPage`.
|
||||
|
||||
### 1.2 CSS und Sonstiges
|
||||
|
||||
| Datei | Zeilen | Besonderheit |
|
||||
|---|---|---|
|
||||
| `src/styles/global.css` | 1.476 | 568 Regeln, 378 Klassennamen, 272 `var()`-Zeilen, 79 `!important`, 18 Gradienten, 104 Hex + 90 rgb(a); 3 Schichten: L1-794 Legacy-Dunkel, L796-1120 "Redesign auf dunklen Tokens", L1122-1476 Rechtliches (L1122-1153) + Finanzen (L1155-1476) |
|
||||
| `src/pages/AdminPage.css` | 416 | 16 Farbliterale, `--admin-line` als Weiß-Alpha; enthält auch geteilte `.credential*`, `.sr-only`, `.spin` |
|
||||
| `src/styles/serverpuls.css` | 74 | 9 Literale; L70-74 `.side-dot*` gehören zur Sidebar |
|
||||
| `src/styles/admin-areas.css` | 61 | 5 Literale, 7 Importeure |
|
||||
| `src/styles/chat.css` | 52 | grün verdrahtet, `var(--surface)` ist undefiniert (Altfehler, L13) |
|
||||
| `src/lib/previewEnv.js` | 29 | 4 Hex (6 Vorkommen) in `dotColor()` |
|
||||
| `src/assets/fonts` | 2 Dateien | Lato 400 (96 KB) + `OFL-Lato.txt` |
|
||||
|
||||
- Tote CSS-Klassen: 66 Namen, etwa 88 Regeln und 285 Zeilen ohne jede Verwendung, dazu 3 tote Keyframes (`slideDown`, `fadeIn`, `slideIn`).
|
||||
- Klassen ohne Regel: `p-4`, `btn-sm` (sitzt in 7 JSX-Dateien!), `status-filter`, `supporters-editor`, `timeline` und rund 35 Bootstrap-Namen in `WorksheetList.jsx` (`d-flex`, `py-3`, `col-md-6`, …). Sie tun heute nichts.
|
||||
- Breakpoints heute: 640, 700, 720 (x3 plus Sidebar-Auto-Collapse), 760, 768, 900, 1060, 1100 und Container Queries 560, 640, 860 und 960. Der Prototyp kennt nur 1100 und 860.
|
||||
- Tests: 34 Dateien, 312 Tests, nur reine Logik (`node --test`, kein jsdom). Genau ein Test greift Markup (`adminRoute.test.mjs`).
|
||||
|
||||
### 1.3 Aufwandsklassen der 104 betrachteten Dateien (JSX, JS, CSS, `index.html`, fremde Chat-Datei)
|
||||
|
||||
| Bereich | trivial | klein | mittel | groß |
|
||||
|---|---|---|---|---|
|
||||
| Grundgerüst/UI/Login | 13 | 7 | 4 | 1 (`global.css`) |
|
||||
| Tickets/Planboard | 4 | 7 | 6 | 3 (`TicketsPage`, `TicketCard`, `WorksheetList`) |
|
||||
| Kunden/Chat/… | 7 | 5 | 8 | 2 (`CustomerDetailPage`, `CreditDetail`) |
|
||||
| Projekte | 0 | 2 | 1 | 1 (`ProjectsPage`) |
|
||||
| Admin/Finanzen/Recht/Puls | 9 | 13 | 10 | 1 (`InvoiceCreateModal`) |
|
||||
| **Summe** | **33** | **34** | **29** | **8** |
|
||||
|
||||
**Triviale Dateien** (nur Klassen oder Text, kein Layout):
|
||||
- Grundgerüst: `main.jsx`, `index.html` (+Favicon), `Footer.jsx` (löschen), `LetterGlitch.jsx` (löschen), `AuthContext.jsx` (keine Änderung), `lib/navLinks.js`, `ui/index.js`, `DateTimeInput`, `DurationSelect`, `EmptyState`, `Field`, `PageHeader`, `AdminRoute` (testgebunden, siehe 7.4).
|
||||
- Tickets: `UrgentToggle`, `PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential`.
|
||||
- Kunden: `KundenansichtButton`, `CreditsPage`, `CreditsView`, `OffersPage`, `OffersView`, `LeadsPage` (löschen), `loadChatComposer.js` (nicht anfassen).
|
||||
- Admin: `FinanceSettingsPage`, `BankCallbackPage`, `LegalPage`, `AddressWarning`, `CustomerInvoices`, `FormField`, `Pager`, `legalUi`, `Sparkline`.
|
||||
- Logik: alle `src/hooks/*`, alle `src/lib/*` bis auf die vier in 5.P1 genannten.
|
||||
|
||||
---
|
||||
|
||||
## 2. Design-Tokens
|
||||
|
||||
### 2.1 Finaler `:root`-Block für WOMS
|
||||
|
||||
Quelle ist `styles.css` L5-44 mit festem Akzent Schwarz. Dazu kommen wenige Ergänzungen für Stellen, an denen der Prototyp nur Literale benutzt.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-hover: #f6f8fa;
|
||||
--bg-subtle: #f8f9fb;
|
||||
|
||||
--text: #1a1f36;
|
||||
--text-2: #5e6678;
|
||||
--text-3: #8a93a3;
|
||||
|
||||
--line: #ebeef1;
|
||||
--line-strong: #d8dde3;
|
||||
|
||||
--accent: #111111;
|
||||
--accent-hover: #2b2b2b;
|
||||
--accent-ink: #111111;
|
||||
--accent-soft: rgba(17, 17, 17, 0.06);
|
||||
--accent-ring: rgba(17, 17, 17, 0.14);
|
||||
|
||||
--grey-bg: #ebeef1; --grey-fg: #414552;
|
||||
--blue-bg: #e0f2fe; --blue-fg: #075985;
|
||||
--orange-bg: #fcedb9; --orange-fg: #a82c00;
|
||||
--violet-bg: #efeafe; --violet-fg: #5b21b6;
|
||||
--green-bg: #d7f7c2; --green-fg: #006908;
|
||||
--red-bg: #ffe7f2; --red-fg: #b3093c;
|
||||
--danger: #c0123c;
|
||||
|
||||
--radius: 6px;
|
||||
--radius-lg: 12px;
|
||||
--ring: 0 0 0 1px rgba(60, 66, 87, 0.16), 0 1px 1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-btn: 0 1px 1px rgba(0, 0, 0, 0.12), 0 2px 5px rgba(60, 66, 87, 0.08);
|
||||
--shadow-pop: 0 0 0 1px rgba(60, 66, 87, 0.06), 0 12px 32px rgba(26, 31, 54, 0.14), 0 2px 6px rgba(26, 31, 54, 0.06);
|
||||
|
||||
--sidebar-w: 232px;
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
|
||||
/* Ergänzungen WOMS (im Prototyp nur als Literale) */
|
||||
--nav-dot: #e5a000; /* .nav-dot */
|
||||
--danger-ring: rgba(192, 18, 60, 0.18); /* aria-invalid statt Rahmenfarbe */
|
||||
--overlay: rgba(26, 31, 54, 0.32);
|
||||
--scrim: rgba(26, 31, 54, 0.24);
|
||||
--appt-akquise: #fdf4e3; --appt-akquise-hover: #fbeccd; --appt-hover: #eef1f4;
|
||||
}
|
||||
html { color-scheme: light; } /* ersetzt `color-scheme: dark` an .datetime-pair (global.css L1077) */
|
||||
```
|
||||
|
||||
**Basis:** `body` 14px/20px, `font-family: var(--font)`, `-webkit-font-smoothing: antialiased`. Dazu `button,input,select,textarea{font:inherit}` (fehlt heute, Native-Controls rendern in der Browserschrift), `a{color:inherit;text-decoration:none}`, `:focus-visible{outline:2px solid var(--accent-ring);outline-offset:2px}`.
|
||||
|
||||
**Typo-Skala:**
|
||||
- h1 28/34 700 (mobil 24/30), Login-h1 24/32 700, Section-h2 16/24 600, Dialog-h3 18/26 600.
|
||||
- Body 14/20, `.small` 13, `.xs` 12/16, `.stat .v` 22/30 600 (mobil 18).
|
||||
- `.num` setzt tabular-nums explizit, weil `body` `tnum 0` hat. Alle `.fin-amount` und Betragsspalten behalten tabular-nums.
|
||||
- Alle heutigen Gewichte 800 (Kpi 28/800, `.fin-fact-big`, `.fin-low`, `.sp-tile-value`, `.page-title` 24/800) werden auf 600/700 gesetzt. Mit echter Systemschrift wirkt 800 viel schwerer als mit der synthetisch gefetteten Lato.
|
||||
- Großbuchstaben mit Letter-spacing (`.sp-tile-label`, `.fin-age-label`, `.ui-table th`, `.area-row.head`, `.admin-table th`, `.admin-eyebrow`, `.sp-alert-level`) entfallen. Kopfzellen sind 12px/500 in `--text-2`. Einzige Ausnahme im Prototyp ist `.day-head .wd` im Planboard.
|
||||
|
||||
### 2.2 Mapping alter Tokens und Legacy-Aliase auf neue Werte
|
||||
|
||||
Die Spalte "Verwendungen" zählt `var(--x)` außerhalb von `global.css` ([geprüft], Python-Zählung). Weil diese 4 Fremd-CSS und 11 JSX-Dateien die alten Namen nutzen, bleiben sie als Aliase bestehen und werden erst in Paket 10 gelöscht.
|
||||
|
||||
| Alter Token | Heute (dunkel) | Neu | Verwendungen außerhalb global.css |
|
||||
|---|---|---|---|
|
||||
| `--bg` | #0e131a | `#ffffff` (Name bleibt) | 0 (global 3) |
|
||||
| `--surface-1` | #151b23 | `var(--bg)` | 6 |
|
||||
| `--surface-2` | #1b2430 | `var(--bg-hover)` | 16 (11 CSS + 5 JSX, u.a. `AssignProjectPage` Auswahlzeile) |
|
||||
| `--surface-3` | #222d3a | `var(--bg)` (Popover bekommt `--shadow-pop`) | 0 |
|
||||
| `--surface-hover` | #243040 | `var(--bg-hover)` | 0 |
|
||||
| `--surface` (nirgends definiert, `chat.css` L13) | transparent (Bug) | `var(--bg)` | 1 |
|
||||
| `--border` | weiß 8 % | `var(--line)` | 20 |
|
||||
| `--border-strong` | weiß 14 % | `var(--line-strong)` | 0 |
|
||||
| `--border-accent` | grün 35 % | `var(--accent-ring)` | 1 (`serverpuls.css`) |
|
||||
| `--text` | #e6edf3 | #1a1f36 | 17 |
|
||||
| `--text-muted` | #9aa7b4 | `var(--text-2)` | 31 |
|
||||
| `--text-faint` | #6b7785 | `var(--text-3)` | 7 |
|
||||
| `--accent` | #10b981 | #111111 | 19 |
|
||||
| `--accent-2` | #059669 | `var(--accent-hover)` | 1 (`chat.css`) |
|
||||
| `--accent-soft` | grün 14 % | `rgba(17,17,17,.06)` | 6 |
|
||||
| `--info` | #3b82f6 | `var(--blue-fg)` | 1 |
|
||||
| `--warn` | #f59e0b | `var(--orange-fg)` (Text; als Fläche entfällt es mit den Kpi-Streifen) | 3 |
|
||||
| `--danger` | #ef4444 | #c0123c (6,19:1 auf Weiß; #ef4444 hatte 3,76:1) | 9 |
|
||||
| `--ok` | #10b981 | `var(--green-fg)` | 5 |
|
||||
| `--sp-1..6` | 4/8/12/16/24/32 | unverändert | 38 |
|
||||
| `--r-sm` / `--r-md` / `--r-lg` / `--r-pill` | 8 / 12 / 16 / 999 | `var(--radius)` / `var(--radius-lg)` / `var(--radius-lg)` / 4px (Badge) | 6 / 4 / 0 / 2 |
|
||||
| `--shadow-1..3` | schwarz 30-40 % | `none` / `var(--shadow-btn)` / `var(--shadow-pop)` | 0 |
|
||||
| `--sidebar-w` | 248px | 232px | 0 |
|
||||
| `--sidebar-w-collapsed` | 72px | entfällt (Rail gibt es nicht mehr) | 0 |
|
||||
| Legacy: `--dark-bg`, `--dark-card-bg`, `--dark-text`, `--green-primary`, `--green-secondary`, `--gray-700/800/900` | Aliase auf Dunkelwerte | `--dark-bg`/`--dark-card-bg` = `var(--bg)`, `--dark-text` = `var(--text)`, `--green-primary` = `var(--accent)`, `--green-secondary` = `var(--accent-hover)`, `--gray-700` = `var(--line-strong)`, `--gray-800` = `var(--text-2)`, `--gray-900` = `var(--text)` | 0 (nur 13 + 6 + 1 Stellen innerhalb alter Legacy-Blöcke in global.css, die Paket 1 löscht) |
|
||||
|
||||
Zwei Hinweise zu den Aliasen:
|
||||
- `--text-3` (#8a93a3) hat auf Weiß nur 3,1:1. Der Prototyp nutzt es nur für Tertiäres (IDs, "–", leere Zustände). Hinweise in Formularen laufen über `.hint` mit `--text-2`. Die Klasse `.faint` darf in Bestandscode bleiben, soll aber nur nicht-essenziellen Text tragen.
|
||||
- `--accent` ist heute oft Linkfarbe (`.fin-link`, `.legal-files a`, `.credit-cols a`, `.area-rows a:hover`, Ticketnummer in `CustomerDetailPage`). Mit Schwarz geht die Link-Erkennbarkeit verloren, und der Hover auf Schwarz wäre unsichtbar. Im Kompat-Block bekommen diese Links `text-decoration: underline; text-decoration-color: var(--line-strong)`, wie `.link` im Prototyp.
|
||||
|
||||
### 2.3 Wie Altcode auf Weiß richtig aussieht (Neutralisierung)
|
||||
|
||||
1. Der `:root` wird ersetzt und der Alias-Block nach 2.2 darunter gelegt. Alles, was nur `var()` benutzt (laut Inventar etwa 94 % der Redesign-Schicht), kippt automatisch.
|
||||
2. Dunkelspezifisches wird nicht über Variablen gerettet, sondern gelöscht oder neu geschrieben (Tabelle 2.4).
|
||||
3. Kompat-Block (Paket 1, wird in Paket 10 gelöscht). Er bildet Altklassen auf den neuen Look ab, damit noch nicht migrierte Dateien lesbar und boxlos sind:
|
||||
- `.ui-card`, `.ui-card.pad`, `.ui-card-head`, `.ui-card-body`: kein Hintergrund, kein Rand, kein Schatten, kein Padding. `.ui-card-head h3` bekommt den Stil von `h2.section`.
|
||||
- `.ui-btn`, `-primary`, `-ghost`, `-danger`, `-sm`: wie `.btn`, `.btn-primary`, `.btn`, `.link-danger`, ohne Größenvariante. Sie sind nötig, weil 7 Stellen die Klassen von Hand schreiben (`TicketProjects` x4, `TicketCard` L137 u.a.).
|
||||
- `.form-control`, `.form-group`, `.form-label` entsprechen `.input`/`.select`/`.textarea`/`.field`/`.field > label`. `select.form-control` bekommt das Chevron, `textarea.form-control` die Textarea-Höhe. Die `!important`-Regeln (global.css L308-346, L1046-1047) werden ersatzlos gestrichen.
|
||||
- `.btn`/`.btn-dark`/`.btn-green`/`.btn-teal`: die Namen kollidieren mit dem Prototyp-`.btn` (weiß mit Ring). Im Bestand ist `.btn` (43x in 13 Dateien) ein graues Gradient-Element mit `!important`. Es wird komplett durch die Prototyp-`.btn`-Regel ersetzt, `.btn-dark` und `.btn-teal` werden `.btn.btn-primary`. `.btn-green` stirbt mit der Login-Seite.
|
||||
- `.badge-muted|info|ok|warn|danger` entsprechen `.badge`, `.blue`, `.green`, `.orange`, `.red`. `.status-open|working|awaiting|assigned|closed` und `.priority-critical` (nur bis `StatusPill` neu ist, in P1 sofort ersetzt).
|
||||
- `.overlay`, `.overlay-close`, `.overlay-content`: `.overlay` wird zum Prototyp-Backdrop (`--overlay`, zentriert). `.overlay-content` wird zur weißen Box (radius 12, `--shadow-pop`, Padding 24, Text `--text` statt `#fff`). `.overlay-close` wird ein kleines graues "×" oben rechts. Alle 10 handgebauten Modals sehen damit sofort weiß aus. Die finalen Dialoge kommen in den Bereichspaketen (3).
|
||||
- `.card`, `.card-header`, `.card-body`, `.row`, `.col`, `.col-6`, `.dropdown*` (bis P2), `.spinner` (neutraler grauer Ring), `.text-grey` (`--text-2`), `.text-red` (`--red-fg`), `.text-white`, `.bg-red` (rot getönte Textzeile ohne Kasten), `.ticket-projects*`.
|
||||
- Utilities `.p-1/.p-2/.mt-2/.mt-3/.mb-2/.text-center/.flex/.gap-2/.gap-3/.items-center/.justify-between/.grow/.wrap/.muted/.faint/.mono/.text-ellipsis` bleiben.
|
||||
4. Chat-Mindestfix in P1, damit der Senden-Knopf lesbar bleibt. `chat.css` bekommt `--chat-accent: var(--accent)`, `--chat-accent-hover: var(--accent-hover)`, `--chat-accent-soft`, `--chat-accent-line: rgba(17,17,17,.14)`, `--chat-accent-text: var(--accent)`, **`--chat-on-accent: #ffffff`** (heute `#04130d`, auf Schwarz wäre die Schrift unsichtbar) und `--chat-surface: var(--bg)`. Der Rand der eigenen Blase wird `var(--line)`, und `.chat-messages` bekommt `background: var(--bg)`, womit der `--surface`-Fehler weg ist.
|
||||
|
||||
### 2.4 `global.css`: Blöcke und Aktion
|
||||
|
||||
| Zeilen | Inhalt | Aktion |
|
||||
|---|---|---|
|
||||
| 1-10 | `@font-face` Lato | löschen, plus `src/assets/fonts/*` |
|
||||
| 18-71 | `:root` | ersetzen (2.1 + 2.2) |
|
||||
| 73-106 | `body`-Glows, `overflow:hidden`, Scrollbar, `::selection` | löschen; `body` auf Prototyp-Basis. Scrollbar und Selection bleiben im System-Standard |
|
||||
| 108-126, 128-140, 142-185 | `.bg-*`/`.text-*` (17 Klassen, 13 tot), `.container`, `.navbar*` | nur `.bg-red`, `.text-red/white/grey` als Kompat behalten, Rest löschen |
|
||||
| 187-233 | `.btn`-Familie (Gradienten, grüner Glow) | ersetzen (Prototyp `.btn`) |
|
||||
| 235-305 | `.table`, `.card` (tot bzw. 2 Verwendungen) | löschen / Kompat |
|
||||
| 307-351 | `.form-control` mit `!important` | ersetzen (Prototyp `.input`, Alias) |
|
||||
| 353-382 | `.row/.col`, Utilities | behalten (Kompat), tote entfernen |
|
||||
| 384-494 | `.ticket-row`, `.worksheet-expansion` usw. | löschen (komplett tot) |
|
||||
| 496-509 | `.status-*`, `.priority-*` | löschen, durch `.badge`-Töne ersetzen (StatusPill) |
|
||||
| 511-560 | `.dropdown*` | bis P2 behalten, dann löschen |
|
||||
| 562-605 | `.overlay*`, `.modal-content` | umbauen (2.3), Rest in P10 löschen |
|
||||
| 607-754 | `.slider`, `.drop-zone`, `.modern-card`, `.footer`, `.spinner` | löschen, außer `.spinner` (neutral) |
|
||||
| 756-787, 1035-1039 | `.ticket-tab*` | durch `.tabs`/`.tab` ersetzen |
|
||||
| 789-794 | Responsive Legacy | löschen |
|
||||
| 796-864 | App-Shell, `.side*` | ersetzen (Prototyp `.shell/.sidebar/.nav-item/...`) |
|
||||
| 866-1033 | Page-Header, `.ui-card`, `.ui-btn`, `.badge-*`, `.kpi*`, `.tcard*`, `.list*`, `.ui-table`, `.seg`, `.empty`, `.field` | ersetzen bzw. in Bereichspaketen löschen (`.tcard*` in P2, `.kpi*` in P7) |
|
||||
| 1050-1120 | Utilities, `.automatic-assignment-note`, `.datetime-pair`, `.autocomplete*` | behalten, neu einfärben; `color-scheme:dark` (L1077) löschen |
|
||||
| 1122-1153 | Rechtliches | in P8 umbauen |
|
||||
| 1155-1476 | Finanzen (26 Dunkel-Literale: `#fecaca` L1377, `#fbbf24` L1379/1383, `rgba(6,9,14,.98)` L1372, `#f87171` L1400, `#b45309` L1144, rgba-Tönungen) | P1 nur Literale entfernen, Umbau in P7 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Bausteine
|
||||
|
||||
Zeile = Baustein. "Verbraucher" nennt Dateien und, wo vom Inventar gezählt, die Anzahl.
|
||||
|
||||
| Baustein | Heute | Neu / Änderung | Verbraucher |
|
||||
|---|---|---|---|
|
||||
| **App-Shell** | `.app-shell` 100vh + `overflow:hidden`, innerer Scroller `.app-scroll`, `Footer` darunter, Sidebar 248px sticky im Flex | `.shell` > `<aside class="sidebar">` (fixed 232px) + `.main` (`margin-left: var(--sidebar-w)`) > `.mobile-bar` + `.page`. Dokument-Scroll (Body scrollt). Footer entfällt. `menuOpen`-State in `AppContent`, Schließen bei Routenwechsel (`useLocation`), Esc und Klick auf `.scrim`. `ScrollToTop`-Effekt bei Routenwechsel (fehlt heute komplett, `rg scrollTo` = 0). Ladezustand: `.loading`-Klasse statt Inline-Style, Text "Wird geladen …" | `App.jsx`, `Navbar.jsx`, `Footer.jsx` (löschen), `AdminRoute.jsx` |
|
||||
| **Sidebar** | Icons (17 Tabler), Gruppen, Collapse-Rail 72px, Avatar-Verlauf, `.side-count` grün | Textliste (`.nav-item`, `.sub`, `.nav-count`, `.hot` für Chat-Ungelesene, `.paused` mit "pausiert", `.nav-dot`), `.sidebar-foot` mit `.who` (Name, Ellipsis) + "Abmelden". Collapse und `lib/sidebar.js` entfallen, Drawer unter 860px. Serverpuls-Punkt: `.nav-dot` (#e5a000) für Stufe medium, zusätzlich `.nav-dot.critical` (`--danger`), da das Live-System zwei Stufen hat. `data-testid="sp-nav-dot"`, `title`, `aria-disabled`, 5-min-Polling und `useSidebarCounts` bleiben | `Navbar.jsx`, `lib/navLinks.js` (nur optional `hot:true` am Chat-Kind), `serverpuls.css` L70-74 |
|
||||
| **Logo** | grüne Kachel mit "W" | neue `components/Logo.jsx` mit dem SVG aus `app.js` (Konstante `LOGO`, Z. 115); 24px in Sidebar/Mobile-Bar, 40px im Login; `public/favicon.svg` aus `quelle/favicon.svg` plus `<link rel="icon">`, `noindex`, `color-scheme` in `index.html` | `Navbar`, `LoginPage`, `index.html` |
|
||||
| **Seitenkopf** (`PageHeader`) | `.page-header/.page-title/.page-subtitle/.page-actions`, 24/800 | Klassen `.page-head`, h1 28/700, `.sub`, `.actions`; neue Props `eyebrow` und `crumb`; mobil gestapelt unter 860px | 12 Verwendungen in 9 Dateien; `ProjectsPage` hat einen eigenen zentrierten `<h2>`-Kopf und muss umgestellt werden |
|
||||
| **Breadcrumb** | gibt es nicht ("Zurück"/"Alle Kunden" mit Pfeil-Icons) | `.crumb` (13px, `--text-2`, Slash in `--text-3`) | Ticket-Seite, `CustomerDetailPage`, `FinanceSettingsPage`, `AssignProjectPage` |
|
||||
| **Toolbar/Suche** | `.toolbar` (2x), `.search-input` (4x), Filterkarte mit "Anwenden" | `.toolbar` + `.input.search` (300px, mobil 100%) + `.select-quiet` + `.check`; Suche live (Debounce oder Enter), "Anwenden" entfällt | `TicketsPage`, `CustomersPage`, `ProjectsPage`, `AppsTable`, `CustomerChatPage`, `ProviderContractsTab` |
|
||||
| **Tabs** | `Tabs.jsx`, `.ticket-tabs/.ticket-tab` (gefüllt, grüner Verlauf), kein Zähler | `.tabs` (Unterstrich 2px `--accent`, `overflow-x:auto`) + `.tab` + `<span class="count">`; neue Prop `t.count`. Auch als Filter-Tabs: Tickets, Kunden, Projekte, Gutschriften, Angebote | 3 Aufrufer heute (`TicketCard`, `CustomerDetailPage`, `LegalPage`); neu mehr. **Achtung:** `customerTabs.js` testet `Object.keys(entry) = [id, label]`, Zähler also nicht in diese Funktion, sondern beim Rendern anfügen |
|
||||
| **Segmented Control** | `.seg` (dunkle Schiene, 4 Aufrufer), `.sp-pill` | `.seg` (graue Schiene, aktiver Chip weiß mit Ring), `aria-pressed` | `CustomersPage` (wird Tabs), `InvoiceTable`, `CreditList`, `OfferList`, Serverpuls-Pills, neu: Einsatz Fernwartung/Vor Ort (`CreateWorksheetModal`), Art-Wahl `AssignProjectPage` |
|
||||
| **Tabelle** | `.ui-table` (4x), div-Raster `.area-row` (4 Ansichten), `.tcard`-Liste, `.list-row` (6x), `.sp-table`, `.admin-table`, `.legal-contracts` | `.table` (Haarlinien, Kopf 12px/500), `tr.row` (klickbar, Hover `--bg-subtle`), `.cell-title`/`.cell-sub`/`.id`, `.right`, `.num`, `.table-foot`, `.hide-sm` (<860) und `.hide-md` (<1100). Container-Query-Stapelung der Finanztabellen bleibt (Wrapper statt Card trägt `container-type`) | Tickets, Kunden, Projekte, Gutschriften, Angebote, E-Mail-Speicher, Funktionen, Finanzen (3), Rechtliches, Admin, Serverpuls |
|
||||
| **Listenzeile** | `.list`/`.list-row` mit Hover-Fläche | Haarlinie, `a.list-row:hover .cell-title { color: var(--accent-ink) }`, erste Zeile `border-top` | `ProjectContextPanel`, `TicketProjects`, `AssignProjectPage`, `ServicesPage`, `CustomerViewLog`, `CustomerChatPage` |
|
||||
| **Facts** | `Ticketkopf` als Meta-Zeile | `.facts`/`.fact` (`.k` 13px, `.v` 500), mit `.select-quiet` für "Zuständig" | Ticket-Seite (neu) |
|
||||
| **Stats** | `Kpi` (Box mit Farbstreifen, 28/800), `WorksheetSummary`-Streifen, `.fin-stats`, `.fin-age` | `.stats`/`.stat` (`.k` 13px, `.v` 22/600, tabular), `.v.warn` orange, **neu `.v.neg` rot** (negatives Geld soll rot, nicht orange, sein); `Kpi` wird `Stat` (Prop `tone` statt `accent`), Icon-Prop und Streifen entfallen | `LiquidityHead` (3), `WorksheetSummary`, `AboCard`, Finance-Altersstreifen, `TaxCard` |
|
||||
| **kv / two-col / prose** | `.area-kv`, `.credit-cols`, `.fin-cols`, `.fin-bank-facts` | `.kv` (180px/1fr, mobil 1 Spalte), `.two-col` (1fr 1fr, Gap 56, mobil 1 Spalte), `.prose` (`pre-wrap`, max 680px) | Kundenübersicht, `CreditDetail`, `AboStatusCard`, `BankAccessCard`, `TaxCard`, Beschreibung des Tickets (die heutige `description` bekommt `pre-wrap`) |
|
||||
| **Badge** | `Badge.jsx` Pille (999px), Tone `muted|info|ok|warn|danger`, `dot`-Prop (5 Aufrufer) | `<span className="badge {tone}">`; Mapping `muted`→(grau), `info`→`blue`, `ok`→`green`, `warn`→`orange`, `danger`→`red`; neu `violet`, `plain`; 20px hoch, Radius 4, kein Rand, `dot` entfällt. Die Töne in `lib/*Format.js` bleiben **unverändert** (Tests deepEqual-en sie), das Mapping sitzt nur in `Badge.jsx` | 44 Verwendungen in 20 Dateien |
|
||||
| **Status-Töne** | `statusClass()` → `.status-*` (Abschnitt unten) | `StatusPill` rendert `badge {tone}`; `UrgentPill` = `badge red` "Dringend" | `TicketCard`, `CustomerDetailPage`, `ProjectContextPanel` |
|
||||
| **Button** | `Button.jsx` (`ui-btn`, 4 Varianten, `size="sm"`), 114 Verwendungen in 35 Dateien (43 ghost, 43 sm, 36 primary, 5 danger) | default und ghost → `.btn`; primary → `.btn.btn-primary`; neue Variante `quiet` → `.btn.btn-quiet`; danger → `.link-danger` (Textlink, im Prototyp gibt es keinen roten Knopf); `size` entfällt (alle 32px, `Button` ignoriert `size` zunächst); Zeilenaktionen in Tabellen werden `.link` | alle; `TicketProjects` u.a. mit handgeschriebenem `ui-btn` |
|
||||
| **Card** | `.ui-card` Box, 57 Verwendungen in 25 Dateien, `bodyStyle`, `pad` | `<section className={className}>` + `<h2 class="section">{title}{actions}</h2>` + Kinder; `pad`/`bodyStyle`/`style` werden No-ops; `className` bleibt (Finance-Wrapper), Container-Queries brauchen das Element | alle |
|
||||
| **Dialog** | 10 handgebaute Overlays (siehe 3.1) + 2 `fin-overlay`; schwarz, weißes 48px-"×" als `<span>`, kein `role`, kein Esc | neue `ui/Dialog.jsx`: `.overlay` > `form|div.dialog` (max 520, Variante breit ~760 für Rechnung) mit `.dialog-head` (h3 + `<button class="dialog-close" aria-label="Schließen">×</button>`), `.dialog-body` (Flex-Spalte, Gap 16, `overflow-y:auto`), `.dialog-foot`; `role="dialog" aria-modal aria-labelledby`, Esc, Klick aufs Backdrop, mobil Bottom-Sheet. Bindet `useDialogFocus` ein (Overlay `onKeyDown`, innerer Dialog mit `ref` + `tabIndex=-1`) | siehe 3.1 |
|
||||
| **Form-Felder** | `.form-group/.form-label/.form-control` (49/49/118), `Field` (32x in 4 Dateien), `.datetime-pair`, `FormField` | `.field > label` (500) + `.input`/`.select`/`.textarea` (32px, Ring-Schatten, Fokus-Ring 2-stufig, **kein Rand**) + `.hint` (13px `--text-2`) + `.field-row`/`.field-row-3`; Fehler: `box-shadow: 0 0 0 1px var(--danger), 0 0 0 4px var(--danger-ring)`; `.check` für Checkbox/Radio | alle Formulare |
|
||||
| **Urgent-Schalter** | `.urgent-toggle` gestrichelte Pille, `.on` | gleiche Klassennamen, aber Markup: `<span class="sw"/>` vor dem Label (28x16 Schalter, `.on` rot); `aria-pressed` bleibt; Logik unverändert | `UrgentToggle.jsx` (verbraucht nur von `TicketCard` bzw. neuer Ticket-Seite) |
|
||||
| **Empty-State** | `EmptyState` mit `icon`-Prop (keiner übergibt es), 3 Inline-Styles | `<div class="empty"><strong>Titel</strong>Hinweis</div>` + Aktion; `icon`-Prop und `.empty-icon` entfernen | 16 Verwendungen in 13 Dateien |
|
||||
| **Autocomplete** | `.autocomplete-list` auf `--surface-3` mit Schatten | weißes Popover (`--bg`, `--shadow-pop`, Radius 8), aktiver/Hover-Eintrag `--bg-hover`, `form-control`→`input`. **Achtung** Clipping im scrollenden `.dialog-body` | `CreateTicketModal`, `InvoiceCreateModal`, `OfferForm`, `CustomerChatPage` |
|
||||
| **Meldungen** | `.bg-red.text-white` (9x in 6 Dateien), `.legal-error`, `.fin-dialog-error`, `.area-error/-message/-summary`, `.sp-error`, `.admin-notice` | **Kein Kasten.** Neue Klassen `.msg`, `.msg.err` (13px `--red-fg`), `.msg.ok` (`--green-fg`), `.msg.warn` (`--orange-fg`); transiente Meldungen optional als `.toast` (dunkle Pille, Prototyp) | siehe Spalte links; im Prototyp gibt es keine Banner, deshalb Text-only |
|
||||
| **Lader** | `.spinner` (40x in 25 Dateien, grüner Ring) + doppeltes `FaSpinner` | Text "Wird geladen …"/"Speichert …" über `.loading` (`color: var(--text-2)`); `.spinner` bleibt nur als neutraler Kompat-Ring bis P10 | alle |
|
||||
| **Login** | Glas-Karte über `LetterGlitch`-Canvas, 19 Inline-Styles | `.login > .login-box` (360px, Logo 40px, h1, `.sub`, `.field`, `.btn-primary.btn-block`), Code-Schritt mit `.code-input`, `.login-links`, `.login-foot` (siehe 5.P1) | `LoginPage.jsx` |
|
||||
| **Tab-Inhalt Ticket** | `.ticket-tab-content` | entfällt (Reiterinhalt direkt unter `.tabs`) | `TicketCard` |
|
||||
|
||||
### 3.1 Dialog-Verbraucher (10 handgebaute Overlays + Sonderfälle)
|
||||
`CreateTicketModal`, `CreateWorksheetModal`, `WorksheetTimesModal`, `OwnSupportModal`, `TicketProjects` (Art-Frage "Was ist das?"), `ProjectsPage` (2x: Neues Projekt, Projekt bearbeiten), `finance/AssignDialog`, `finance/InvoiceCreateModal`, `legal/ContractForm`, `legal/VersionForm`.
|
||||
Weitere Kandidaten:
|
||||
- `CreateCustomerForm` (aus `CustomersPage.jsx` exportiert, in `AssignProjectPage.jsx` L12/L277 inline benutzt).
|
||||
- `OfferForm` (Inline-Karte).
|
||||
- `AnalyseRow` (Zustimmungsablauf).
|
||||
Es gibt keinen Teil des Altcodes, der `.overlay` ohne die drei Klassen `overlay`, `overlay-close`, `overlay-content` benutzt. Deshalb funktioniert die CSS-Zwischenlösung (2.3).
|
||||
|
||||
### 3.2 Status-Töne (`lib/ticketStatus.js`, ungetestet, neu mit Test)
|
||||
|
||||
| DB-Wert (gespeichert, englisch bleibt) | Gruppe | heute | neu (Prototyp) |
|
||||
|---|---|---|---|
|
||||
| `Open` | open | `.status-open` (grün) | grau (kein Ton) |
|
||||
| `Occupied`, `Assigned`, `Added Info`, `In Kontakt` | working | blau | blau |
|
||||
| `Awaiting`, `Halted` | waiting | amber | orange |
|
||||
| `In Test` | review | `.status-assigned` (teal) | violett |
|
||||
| `Closed`, `Zugesagt` | done | grau (`.status-closed`) | grün |
|
||||
| `Cancelled`, `Aborted`, `Abgesagt` | cancelled | grau (`.status-closed`) | `plain` (nur Text, `--text-3`) |
|
||||
|
||||
- `done` und `cancelled` teilen sich heute `status-closed` und müssen getrennt werden.
|
||||
- Implementiert wird eine neue Funktion `statusTone(status)` in `lib/ticketStatus.js`. Sie liefert `'' | 'blue' | 'orange' | 'violet' | 'green' | 'plain'`, `statusClass()` entfällt. Neuer Test `tests/unit/ticketStatus.test.mjs`.
|
||||
- Das ist ein **sichtbarer Bedeutungswechsel** (Offen war grün, Erledigt grau). Er ist im Prototyp so freigegeben, wird aber in Abschnitt 8 erneut genannt.
|
||||
- Weitere Töne-Vokabulare, die in Komponenten gemappt werden und deren lib-Tests dadurch unberührt bleiben:
|
||||
- `previewStatus().color` (`green|yellow|red|grey`), Mapping in `PreviewBlock`: yellow→orange.
|
||||
- `analyseStatus().color` (`green|red|grey`).
|
||||
- `worksheetTone()`: `git-open`→`badge orange` "Zeit offen", `git-done`→`badge green` "Zeit eingetragen", `comment`→Zeile `.ws.comment`, `work`→kein Badge.
|
||||
- `deadlineTone()` (legal).
|
||||
- `formatDelta().dir` (Serverpuls).
|
||||
- `ageLabel().stale`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Icon-Regel
|
||||
|
||||
Regel: Icons nur dort, wo sie handeln, und auch dann als Zeichen statt Bibliothek. Konsequenz: **keine einzige Icon-Komponente bleibt**, und beide Bibliotheken fliegen raus.
|
||||
|
||||
Heute: 71 verschiedene Icons in 187 Verwendungen in 51 Dateien (17 `@tabler/icons-react` in `Navbar.jsx` und `ProjectContextPanel.jsx`, 53 `react-icons` (fa6/fa), 1 `react-icons/si` = `SiGitea`). Mit `LeadsPage` (25 Tags) entfallen davon nach Paket 0 bereits 25.
|
||||
|
||||
### A. Entfernen (dekorativ, der Text daneben genügt)
|
||||
|
||||
| Icon(s) | Stellen |
|
||||
|---|---|
|
||||
| 14 Menü-Icons (`IconTicket`, `IconCalendar`, `IconFolders`, `IconLayoutGrid`, `IconUsers`, `IconMessages`, `IconReceiptRefund`, `IconDiscount`, `IconMailCog`, `IconTargetArrow`, `IconReportMoney`, `IconScale`, `IconActivityHeartbeat`, `IconSettings`) | `Navbar.jsx` |
|
||||
| `IconLayoutSidebarLeftCollapse/Expand` | `Navbar.jsx` (Funktion entfällt) |
|
||||
| `IconFolders` | `ProjectContextPanel.jsx` |
|
||||
| `FaPlus` (15) | `TicketsPage` x2, `TicketCard` (Arbeitsblatt hinzufügen), `TicketProjects`, `FixedCostsCard`, `InvoiceCreateModal`, `InvoiceTable`, `OffersView`, `AdminPage`, `AssignProjectPage` x2, `CustomersPage` x2, `ProjectsPage` (dort icon-only, siehe B) |
|
||||
| `FaFloppyDisk` (4), `FaGear`, `FaSliders`, `FaAngleDown`, `FaCodeBranch`, `FaCircleInfo`, `FaFolder` x2, `FaLock`, `FaClone`, `FaKey`, `FaArrowUp` | `SettingsCard`, `AdminPage`, `CustomerDetailPage`, `FinancePage`, `TicketsPage`, `AssignedProjectCard`, `ProjectsPage` |
|
||||
| `FaClock` x4, `FaUser` x2, `FaExchangeAlt`, `FaComment` x2, `FaHandsHelping`, `FaPen`, `FaCheck`, `FaChevronDown/Up` (pro Arbeitsblatt) | `WorksheetList.jsx` (Aufklapp-Mechanik entfällt) |
|
||||
| `FaPlay`, `FaStop` | `TicketCard.jsx` (werden zu Text "Beginn"/"Fällig" in den Facts) |
|
||||
| `FaTriangleExclamation` (7) | `AboCard` x2, `ForecastTable`, `StatusLine`, `TransactionTable` (die Wörter "gefährdet", "Hinweis" tragen die Bedeutung) |
|
||||
| `FaArrowUpRightFromSquare` x2 | `CreditDetail`, `KundenansichtButton` |
|
||||
| `FaBoxArchive` (3), `FaBoxOpen` (2), `FaTrash` | `CustomerDetailPage`, `ProjectsPage` (Archiv-Knöpfe als Text), `AdminPage` (siehe B) |
|
||||
| `FaStar` x2 ("Wichtiger Kunde") | `CustomersPage`, `CustomerDetailPage` (wird `.badge` "Wichtig") |
|
||||
| `FaArrowLeft` x5, `FaArrowRight` x2 | `AssignProjectPage`, `CustomerDetailPage` x2, `FinanceSettingsPage`, `CustomerChatPage` ("Zurück", "Weiter", Breadcrumb tragen den Sinn; mobile Chat-Rückkehr als Text "← Alle Unterhaltungen") |
|
||||
| `FaRotateRight` x4, `FaRotate` x2 | "Erneut versuchen", "Jetzt abrufen", "Aktualisieren" bleiben als reiner Text |
|
||||
| `Badge.dot` (5 Aufrufer), `EmptyState.icon`, `Kpi.icon` | `CustomersPage` x2, `CustomerDetailPage` x3; Slots ohne Aufrufer |
|
||||
|
||||
### B. Durch Text ersetzen (heute icon-only oder reiner Lader)
|
||||
|
||||
| Icon | Wo | Ersatz |
|
||||
|---|---|---|
|
||||
| `FaSpinner` (37) | 25 Dateien (Lader und Busy-Zustand in Knöpfen) | "Wird geladen …", "Speichert …", "Sendet …" |
|
||||
| `FaEdit`, `FaTrash` | `AdminPage` Zeilenaktionen | `.link` "Bearbeiten", `.link-danger` "Löschen" |
|
||||
| `FaTrashCan` | `InvoiceCreateModal` Positionszeile | `.link` "Entfernen" |
|
||||
| `FaEye` | `PreviewLinkButton` (holt Einmal-Link, `onClick` bleibt) | `.link` "Vorschau öffnen" |
|
||||
| `SiGitea` | `GiteaLinkButton` | `.link` "In Gitea öffnen" |
|
||||
| `FaLinkSlash` | `AssignedProjectCard` | `.link-danger` "Lösen" |
|
||||
| `FaUserPlus` | `EditorDropdown` (wird `<select>`) | Option "Nicht zugewiesen" |
|
||||
| `FaChevronLeft/Right` | `PlanboardPage` | Zeichen ← und → neben "Heute" |
|
||||
| `FaGlobe`, `FaArrowsRotate` x2, `FaPen`, `FaPlus` (icon-only) | `ProjectsPage` (hosten, abgleichen, neu deployen, bearbeiten, neu) | Textknöpfe/`.link` |
|
||||
| `FaEye/FaEyeSlash/FaCopy/FaCheck` | `CopyableCredential` (handelnd, daher Entscheidung offen; Standard Text) | "Anzeigen/Verbergen", "Kopieren/Kopiert" |
|
||||
| `FaCopy/FaCheck` | `CustomerFunktionen` (Site-Key) | "Kopieren/Kopiert" |
|
||||
| Text-Glyphen `✓ Abo`, `+ Abo`, `↳ Unterstützung`, `→` (Status), `✓` (Berechnung), `✓ ! i` in Badges, `▲/▼` (Deltas), `▴/▾` (Preview-Toggle), Emoji `🔴🟠🟢` | `WorksheetList`, `CreateWorksheetModal`, `WorksheetTimesModal`, `CreditDetail`, `AppsTable`/`ServerpulsPage`, `AssignedProjectCard` | "Abo"/"+ Abo", "Unterstützung", "Status danach: X", Text-Badge, Vorzeichen der Zahl, "Vorschau anzeigen/ausblenden", Badge "Alles ok/Warnungen/Critical" |
|
||||
|
||||
### C. Behalten, aber als Zeichen (keine Komponente)
|
||||
|
||||
| Zweck | Umsetzung |
|
||||
|---|---|
|
||||
| Dialog schließen (7 `FaTimes` + 3 `FaXmark` in Dialogen/Bannern) | `<button class="dialog-close" aria-label="Schließen">×</button>` (muss ein echter Button sein für Tastatur und `useDialogFocus`) |
|
||||
| Sortierpfeile, Auf-/Zuklappen | Zeichen ↑ ↓ (`AppsTable`), ▾ ▴ (`LiquidityHead`) |
|
||||
| Chat-Widget (Anhang, Kamera, Mikrofon, Senden, Play, Download usw.) | Inline-SVGs aus dem **fremden** Portal-Code (`chat-composer.js`, 15 Stück), nicht änderbar; Anhang-Menü-Icons optional per CSS ausblenden |
|
||||
|
||||
### D. package.json danach
|
||||
|
||||
Beide Pakete können entfernt werden: `@tabler/icons-react` (^3.36.0) und `react-icons` (^4.12.0). Alle 50 Importdateien (nach LeadsPage-Löschung) verlieren ihre Importe. Zum Schluss `rg "react-icons|@tabler" src` = 0 Treffer, dann `npm uninstall` und die Lock-Datei prüfen.
|
||||
- `uplot` bleibt (Serverpuls).
|
||||
- Beim lokalen `npm uninstall` entfernt npm `node_modules/react-icons`, die 147 getrackten Dateien erscheinen als Löschung (siehe 7.6).
|
||||
- Der Tabler-Import sitzt nur in 2 Dateien. Das Löschen aller `Fa*`-Imports ist der Aufwand (162 Icon-Tags in 50 Dateien).
|
||||
|
||||
---
|
||||
|
||||
## 5. Arbeitspakete
|
||||
|
||||
### Zusammenfassung
|
||||
|
||||
| Paket | Inhalt | Aufwand (PT) | Der Eigentümer sieht danach |
|
||||
|---|---|---|---|
|
||||
| **P0** | Vorbereitung, Totcode weg, Test-Harness | 0,5 | nichts |
|
||||
| **P1** | Tokens, Shell, Primitives, Login, Kompat, Mini-Fixes | 3,5-4 | die ganze App ist weiß, Sidebar nur Text, Karten ohne Box, Login neu |
|
||||
| **P2** | Tickets: Liste als Tabelle, eigene Ticket-Seite, Arbeitsblätter, Dialoge | 5-5,5 | Ticket-Hauptarbeitsplatz im neuen Design |
|
||||
| **P3** | Planboard | 0,5 | Wochenraster ohne Karten |
|
||||
| **P4** | Kundenliste und Kundenseite (9 Reiter), Funktionen/Abo/Protokoll | 2,5-3 | Kundenbereich im neuen Design |
|
||||
| **P5** | Projekte, Zuordnen-Assistent, Services, Projekt-Bausteine | 3,5-4 | Projektbereich im neuen Design |
|
||||
| **P6** | Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher | 4 | Rest von Kunden-Unterseiten |
|
||||
| **P7** | Finanzen | 4-4,5 | Finanzseiten ohne Karten |
|
||||
| **P8** | Rechtliches und Admin | 2 | letzte Admin-Flächen |
|
||||
| **P9** | Serverpuls | 1-1,5 | Dashboard hell, Charts hell |
|
||||
| **P10** | Aufräumen (Icon-Pakete, Kompat, tote CSS) | 1 | Bundle kleiner, nichts Neues |
|
||||
| | **Summe** | **27,5-30,5** | |
|
||||
|
||||
PT = Entwicklertage inklusive Selbsttest und Screenshot-Abgleich.
|
||||
|
||||
**Ablauf für jedes Paket:**
|
||||
1. Arbeit im Worktree `dev-design`, Branch `feature/design-weiss`. Nach jedem Paket gibt es einen eigenen Commit-Stapel mit Tag (zum Beispiel `design-p2`).
|
||||
2. Definition of Done:
|
||||
- `npm test` grün (312 oder mehr),
|
||||
- `npm run build` grün (Bundle-Größe notiert),
|
||||
- Vorher-/Nachher-Screens (1440px und 390px),
|
||||
- keine neuen Inline-Styles außer Laufzeitwerten,
|
||||
- keine Icon-Importe im Bereich,
|
||||
- deutsche Texte unverändert (Diff-Stichprobe),
|
||||
- `git add` nur explizite Pfade.
|
||||
3. Live gebracht wird vom Eigentümer mit `git -C <worktree> push origin feature/design-weiss:test` (Fast-Forward). Rückgängig mit Revert-Commit, ein Force-Push ist nicht nötig.
|
||||
4. P7, P8 und P9 sind voneinander unabhängig und können parallel in Worktrees entstehen. Deployt wird nacheinander.
|
||||
|
||||
---
|
||||
|
||||
### P0: Vorbereitung und Totcode (0,5 PT, unsichtbar)
|
||||
|
||||
- **Dateien:**
|
||||
- Worktree `dev-design` von `test`.
|
||||
- `tp2` (1 Commit) vorher in `test` bringen, siehe 7.5.
|
||||
- Löschen: `src/pages/LeadsPage.jsx` (614 Zeilen, 53 Inline-Styles, 25 Icon-Tags), `src/hooks/useLeads.js` (282 Zeilen).
|
||||
- Bleiben: Route `/leads` mit Redirect, pausierter Menüeintrag, `lib/leads.js`.
|
||||
- **Test-Harness** nach dem Rezept in `woms-ticketsystem.md` neu bauen (esbuild-Bundle, Playwright mit gemocktem Appwrite, unter `systemd-run` mit Speichergrenze) und "Vorher"-Screens für etwa 15 Schlüsselseiten erzeugen.
|
||||
- **Test-Auswirkung:** keine (kein Test importiert `LeadsPage` oder `useLeads`).
|
||||
- **Sichtbar:** nichts.
|
||||
|
||||
### P1: Fundament, Tokens, Shell, Primitives, Login (3,5-4 PT)
|
||||
|
||||
Danach ist die ganze App weiß und boxlos, auch wenn die Seiten innen noch alte Layouts haben.
|
||||
|
||||
- **Dateien und Änderungen:**
|
||||
- `src/styles/global.css`:
|
||||
- Tokens (2.1), Alias- und Kompat-Block (2.2 und 2.3).
|
||||
- Löschen: Lato, Glows, Scrollbar und Selection, tote Regeln (66 Namen, rund 88 Regeln, 285 Zeilen), Navbar, Ticket-Row, Slider, Dropzone, Modern-Card, Footer.
|
||||
- Neu anlegen: Basis, `.shell`, `.sidebar`, `.page`, `.mobile-bar`, `.scrim`, `.page-head`, `.crumb`, `h2.section`, `.btn*`, `.badge*`, `.tabs`, `.input/.select/.textarea/.check`, `.toolbar`, `.table*`, `.empty`, `.facts/.stats/.kv/.two-col/.prose`, `.urgent-toggle`, `.list-row`, `.seg`, `.overlay/.dialog*`, `.field`, `.toast`, `.msg*`, `.loading`, `.login*`, Autocomplete-Popover, `.datetime-pair`.
|
||||
- Nicht übernehmen: `.proto*`, `html[data-accent]`, `.nav-gap`. `.ws*` kommt mit P2, `.week/.day/.appt` mit P3.
|
||||
- `index.html`, neu `public/favicon.svg`.
|
||||
- `App.jsx`: Shell-Markup, `menuOpen`, `ScrollToTop`, `.loading`-Block. Routen und Auth unverändert.
|
||||
- `Navbar.jsx` plus neue `Logo.jsx`, Löschen von `Footer.jsx`, `LetterGlitch.jsx`, `lib/sidebar.js` und `tests/unit/sidebar.test.mjs`. `serverpuls.css` L70-74 (`.side-dot*`) wandert in die Sidebar-Regeln.
|
||||
- `AdminRoute.jsx`: nur den Inline-Style gegen `.loading` tauschen, das Markup `<p>Wird geladen …</p>` bleibt wörtlich (Test, siehe 7.4).
|
||||
- `LoginPage.jsx`:
|
||||
- Prototyp-Markup (`.login`, `.login-box`, Logo 40px, `h1` "Anmelden", `.sub`, `.field`, `.input`, `.btn.btn-primary.btn-block` "Weiter").
|
||||
- Code-Schritt (`h1` "Code eingeben", `.code-input`, `.login-links`: "Code erneut senden" · "Abbrechen") plus Unterschritt "Code senden" (fehlt im Prototyp, bleibt) und `.login-foot`.
|
||||
- Labels deutsch ("E-Mail", "Passwort"), `autocomplete`-Attribute ergänzen.
|
||||
- Logik (`login`, `verifyCode`, `sendCode`, `cancelMfa`, `NO_STAFF_ACCESS`, `safeReturnPath`) bleibt exakt.
|
||||
- `components/ui/*`: `Badge`, `Button`, `Card` (zu `section`), `Tabs`, `PageHeader`, `EmptyState`, `Field`, `Kpi` (zu `Stat`), `StatusPill`, `AutocompleteInput`, `DateTimeInput`, `DurationSelect` (Klassenwechsel), neu `Dialog.jsx`, `index.js` ergänzen. `lib/ticketStatus.js`: `statusTone()` plus neuer Test.
|
||||
- **Pflicht-Mini-Fixes, damit nichts unlesbar wird** (je Minuten bis eine Stunde):
|
||||
- `chat.css` Variablen (2.3 Punkt 4).
|
||||
- `AssignedProjectCard.jsx` L27 (Label `#cbd5e0`) und L82 (Hintergrund `rgba(30,41,59,.5)` mit blauem Rand) auf `--text-2` und `--bg-subtle`.
|
||||
- `Chart.jsx` L28-36 (Achsen `#9aa7b4` → `#5e6678`, Grid `rgba(255,255,255,.06)` → `#ebeef1`, Ticks → `#d8dde3`, Serien-Default `#10b981` → `#111111`).
|
||||
- Hell-auf-Dunkel-Textfarben in `admin-areas.css` (`#f87171`, `#fbbf24`), `serverpuls.css` (`#fca5a5`, `#fbbf24`), `AdminPage.css` (`#6ee7b7`, `#fca5a5`, `#34d399`) und `global.css` Finance/Legal (`#fecaca`, `#fbbf24`, `#f87171`) auf `--red-fg`/`--orange-fg`/`--green-fg`.
|
||||
- `PreviewBlock`-Log (`rgba(0,0,0,.35)`) bleibt unschön, aber lesbar bis P5.
|
||||
- **Test-Auswirkung:** `sidebar.test.mjs` entfällt (312 → 309, dann +neu `ticketStatus.test.mjs`). `adminRoute.test.mjs` bleibt grün, solange das `<p>` unverändert ist. Alle anderen Tests sind lib-Tests.
|
||||
- **Der Eigentümer sieht:** weiße App, Sidebar nur als Textliste mit Zählern (ohne Einklappen, auf dem Handy ein Menü-Knopf), neue Statusfarben, schwarze Primärknöpfe, weiße Dialoge (zunächst als Zwischenlösung), neues Login. Listen und Karten innen sehen noch nach altem Layout aus (Ticketkarten, Hover-Dropdowns, Pillen-Raster), sind aber weiß und lesbar.
|
||||
- **QA-Pflichtliste (390px und 1440px):** Login, Ticket-Liste mit aufgeklappter Karte, Arbeitsblätter, Kundenseite (alle 9 Reiter), Chat (Blase, Composer, Senden-Knopf), Projekte (Karten, 2 Dialoge inklusive `PreviewBlock`), Zuordnen, Services, Planboard, Finanzen (alle Blöcke, 2 Dialoge), Rechtliches, Serverpuls (Charts), Admin, Gutschriften/Angebote/Speicher.
|
||||
|
||||
### P2: Tickets: Liste, Ticket-Seite, Arbeitsblätter, Dialoge (5-5,5 PT, davon Route 2-2,5)
|
||||
|
||||
Empfehlung "eigene Seite" (siehe Abschnitt 6). Bei Variante "aufklappen" entfallen etwa 1,2 PT.
|
||||
|
||||
- **Dateien:**
|
||||
- `pages/TicketsPage.jsx`: Tabs (Aktiv/Offen/In Arbeit/Wartet auf Kunde/Prüfung/Erledigt/Alle) plus `.toolbar` (Suche live, Art-Select, "Nur dringende") plus `.table` (Ticket | Status | Zuständig | Termin | Zeit | Erstellt) plus `.table-foot`. Die Liste bleibt beim Nachladen sichtbar (heute wird sie bei jedem Refetch durch einen Spinner ersetzt und entlädt alle Karten). Filterzustand in die URL.
|
||||
- Neu `pages/TicketPage.jsx` (Route `/tickets/:woid/:tab?`) plus Hook `useWorkorder(woid)` (nutzt `Query.equal('woid',[String(woid)])`, wie die heutige Direktsuche). Loading, Fehler und "Ticket nicht gefunden". Header mit `.crumb`, `.eyebrow` ("woid · Art"), h1 = Betreff, `.sub` = Kunde als Link, `.actions` = `UrgentToggle` + Status-`<select>` + primärer "Arbeitsblatt hinzufügen". Dann `.facts` (Zuständig-Select, Termin mit Uhrzeit, Fällig, Einsatz, Angefragt von, Erstellt), `.tabs` Details | Arbeitsblätter(n).
|
||||
- `TicketCard.jsx` entfällt, ihre Zustände und Hooks wandern in `TicketPage` (`useWorksheets(ticket.woid)` immer, `useCustomerAbo` nur wenn Reiter oder Dialog es braucht). `handleCreateWorksheet` (Status-Rückschreiben und Akquise-Automatik für `customerStatus` bei Zugesagt/Abgesagt) wird **vorher** in `lib`/Hook extrahiert und mit einem Test abgesichert, weil es heute ungetestete Geschäftslogik ist.
|
||||
- `WorksheetList.jsx`: komplett neu als `.ws`-Zeilen (Raster 148px | 1fr | auto, Haarlinie) ohne Gradienten, ohne Aufklappen, ohne Staffel-Animation, ohne JS-Hover und ohne die 35 toten Bootstrap-Klassen. `WorksheetSummary` wird `.stats` (Arbeitszeit gesamt, davon Abo-Stunden (neu berechnet aus `countsToPlan`), Zeiten offen mit `.warn`). Reihenfolge bleibt "neueste zuerst". `formatTime` ist doppelt vorhanden und wird vereinheitlicht ("2:15 h").
|
||||
- Dialoge auf `<Dialog>`: `CreateTicketModal`, `CreateWorksheetModal`, `WorksheetTimesModal`, `OwnSupportModal`, `SupportersEditor` (Zeile aus Select + Input + "= 30 min" + `.link` "Entfernen"; im 520-px-Dialog zweizeilig).
|
||||
- `StatusDropdown` und `EditorDropdown` werden native `<select class="select">`. **Wichtig:** der aktuelle Wert muss immer als Option vorhanden sein, auch wenn er nicht in `TICKET_STATUSES` steht (Akquise `In Kontakt/Zugesagt/Abgesagt`, alte `Assigned/Added Info/Halted/Aborted`), sonst zeigt das Select stillschweigend "Offen".
|
||||
- `UrgentToggle.jsx`: Schalter-Markup.
|
||||
- `ProjectContextPanel.jsx` wird der Abschnitt "Weitere Tickets von <Kunde>" (immer sichtbar, "Keine weiteren Tickets.").
|
||||
- `App.jsx`: neue Route; neue `lib/ticketRoutes.js` mit `ticketPath(woid, tab)`.
|
||||
- Alle 7 Deep-Link-Stellen auf `ticketPath()`: `PlanboardPage` L80, `AssignProjectPage` L156 und L405, `ServicesPage` L72, `ProjectContextPanel` L70, `CustomerFunktionen` L104 (`LeadsPage` entfällt in P0). `TicketsPage` behält `?woid=N` als `<Navigate replace to="/tickets/N">`.
|
||||
- Löschen: `.tcard*`, `.urgent-toggle`-Altregeln, `.dropdown*`, `.worksheet-summary`, `.chevron`, `.ticket-tab-content`.
|
||||
- Der Projekt-Block (`TicketProjects`, `AssignedProjectCard`, `PreviewBlock`, Link-Buttons, `AnalyseRow`) bleibt hier unverändert (nur P1-Mini-Fixes) und kommt in P5.
|
||||
- **Zusatzaufwand (optional, +0,5 PT, nur Leseabfragen):** Tab-Zähler (7 `Query.limit(1)`-Abfragen, Wert `total`), Spalte "Zeit" (eine gebündelte Arbeitsblatt-Abfrage pro Seite mit `worksheetMinutes`), Spalte "Zuständig" aus der Mitarbeiterliste statt `assignedName` (das nach Neuzuweisung veraltet).
|
||||
- **Test-Auswirkung:** keine bestehenden Tests betroffen. Neu: Tests für `statusTone` (aus P1), die extrahierte Worksheet-Logik und die Filter-zu-URL-Helfer.
|
||||
- **Sichtbar:** Ticketliste als schlichte Tabelle mit Tabs, Klick öffnet eine eigene Seite mit Facts, Reitern und flachen Arbeitsblatt-Zeilen. Alle Modals sind echte weiße Dialoge mit Esc und Klick außerhalb.
|
||||
|
||||
### P3: Planboard (0,5 PT)
|
||||
|
||||
- **Dateien:** `pages/PlanboardPage.jsx`.
|
||||
- **Änderungen:**
|
||||
- `.week` als CSS-Klasse (7 Spalten, Haarlinien, Wochenende `--bg-subtle`, heute = schwarze Datumspille, `.appt`-Kacheln als `<Link>`, Akquise `--appt-akquise`, unter 860px einspaltig).
|
||||
- Kopf mit Untertitel "Kalenderwoche N · 5. Oktober bis 11. Oktober · n Termine" (`getISOWeek`, `de`-Locale), Aktionen ← / Heute / →.
|
||||
- Die 12 Inline-Styles verschwinden.
|
||||
- **Zu entscheiden:** stornierte Tickets ausblenden (Prototyp) oder wie heute zeigen.
|
||||
- **Test-Auswirkung:** keine. Planboard-Logik (`groupAppointmentsByDay` usw.) bleibt.
|
||||
- **Sichtbar:** Wochenansicht ohne Karten.
|
||||
|
||||
### P4: Kundenliste und Kundenseite (2,5-3 PT)
|
||||
|
||||
- **Dateien:**
|
||||
- `pages/CustomersPage.jsx`: Tabs (Feste Kunden/Leads/Archiviert/Alle mit Zählern) + Suche + `.table` (Unternehmen mit `.cell-sub` Ansprechpartner | Kundennummer | Standort | E-Mail | Telefon | Offene Tickets), "Neuer Kunde" primär, "Neuer Lead" und die Admin-Beschränkung bleiben. Avatar-Kachel und Stern entfallen ("Wichtig" als Badge).
|
||||
- `CreateCustomerForm` wird `<Dialog>`. **Kopplung:** `AssignProjectPage.jsx` L12/L277 nutzt die Komponente inline und muss an dieser einen Stelle in P4 mitgezogen werden.
|
||||
- `pages/CustomerDetailPage.jsx`: Crumb ("Kunden / Name"), Eyebrow (Kundennummer · Stufe als Klartext), h1 nur der Name, 9 Reiter als `.tabs` mit horizontalem Scroll. Übersicht als `.two-col` mit `h2.section` + `dl.kv` (Stammdaten, Rechnungsdaten, Portal-Zugang). Tickets-Reiter als `.table` mit `tr.row`-Links, Projekte-Reiter bis P5 mit P1-Mini-Fixes.
|
||||
- `CustomerFunktionen.jsx` (Raster-Pseudo-Tabelle → `.table`, Status als Badge-Ton, Aktionen als `.link`; hier landet der `tp2`-Commit, siehe 7.5), `AboStatusCard.jsx`, `CustomerViewLog.jsx`, `KundenansichtButton.jsx` (Standard-Variante `.btn` statt primary, damit pro Kopf nur 1 schwarzer Knopf bleibt).
|
||||
- `admin-areas.css`: Grundstil neu (Haarlinien, `.area-kv` ≙ `.kv`, `.area-muted/-danger/-warn` → `--text-2/--red-fg/--orange-fg`, Tint-Kästen raus), die Raster der Gutschriften/Angebote/Speicher folgen in P6.
|
||||
- `lib/customerTabs.js` bleibt unverändert.
|
||||
- **Test-Auswirkung:** keine (`customerTabs.test.mjs` pinnt `id`/`label`).
|
||||
- **Sichtbar:** Kundenbereich ohne Karten, mit Tabelle und `kv`-Listen.
|
||||
- **Entscheidung:** Stammdaten bleiben wie heute immer editierbar (im `kv`-Layout), oder der Prototyp-Umschalter "Bearbeiten" wird übernommen (Standard: bleiben editierbar).
|
||||
|
||||
### P5: Projekte, Zuordnen-Assistent, Services, Projekt-Bausteine (3,5-4 PT)
|
||||
|
||||
- **Dateien:**
|
||||
- `pages/ProjectsPage.jsx`: `PageHeader` + "Gitea abgleichen" (`.btn`) + "Neues Projekt" (primär), Tabs (Aktiv/Vorlagen/Archiviert), `.toolbar` mit Suche + Kunde-Select (+ Mitarbeiter-Select, siehe Abschnitt 8), eine `.table` (Projekt mit `.cell-sub` Repo | Kunde | Art | Adresse | Ticket als `.link` | Status-Badge | Letzter Push), Fuß "n Projekte". Klick öffnet den Dialog.
|
||||
- **Alle heutigen Steuerelemente müssen erhalten bleiben** (Liste in der Projekt-Inventur): Suche, 6 Ansichten, Kunde- und Mitarbeiter-Filter, Kunde-Select je Projekt (inklusive `window.confirm`), Hosten/Aktualisieren/Preview/Gitea, "Weitere Treffer"-Links, `notice`-Zeile, Fehlerbanner.
|
||||
- Neu-Dialog: Anzeigename, Subdomain, **Repo-Name** (im Prototyp nicht vorhanden), Kunde. Bearbeiten-Dialog: Name, Typ, Subdomain mit Ergebnis-Hinweis, Archivieren/Reaktivieren (`.link-danger`, `window.confirm` bleibt), "Speichern & synchronisieren".
|
||||
- `PreviewBlock` steht **außerhalb** des `<form>` (sonst löst Enter im Hauptdialog Speichern aus). Der Dialog wird breiter (`.dialog-wide`) oder der Block ein eigener Abschnitt.
|
||||
- `fetchAllProjects` zeigt "wird geladen" nur beim Erstladen (heute springt die Liste nach jeder Aktion).
|
||||
- `AssignProjectPage.jsx`: `.crumb` + h1 "Projekt zuordnen" + `.sub`; die Header-Card entfällt, jeder Schritt = `h2.section` + Inhalt; Art-Wahl als `.seg`/`.btn`-Reihe, Ja/Nein als zwei `.btn`, Kunden- und Ticketliste als `.list-row` mit `--bg-hover`-Auswahl **(Auswahl nicht nur über das alte `var(--surface-2)`, sondern auch textlich)**; Zusammenfassung als `.kv`; Preview-Zeile mit `.check` und `.field`/`.hint`. Reine Logik (`nextStep/prevStep/canFinish/openTicketsFor`, R9-Regel, 409) bleibt.
|
||||
- `ServicesPage.jsx`: `h2.section` je Art mit Zähler, Projektzeilen als `.list-row`, Feature-Badges (Verfügbar grün/In Entwicklung orange), Preview-Punkt → Badge (Online grün, Wird gebaut orange, Fehler rot, Aus plain).
|
||||
- Projekt-Bausteine: `TicketProjects`, `AssignedProjectCard` (zugleich in `CustomerDetailPage`), `AnalyseRow` (Zustimmung wird `<Dialog>`, Wortlaut aus `lib/analyseView.js` **unverändert**), `PreviewBlock` (Statuspunkt → Badge, Fehlertext `--red-fg`, Log auf `--bg-subtle`, 27 Inline-Styles in 4-5 Klassen), `PreviewLinkButton`, `GiteaLinkButton`, `CopyableCredential` (auch von `CustomerDetailPage`, `AdminPage`, `CustomerFunktionen` genutzt, Signatur von `copyText()` unverändert).
|
||||
- `lib/previewEnv.js`, `lib/previewView.js`: Vokabular bleibt, die Zuordnung auf Badge-Ton passiert in der Komponente (so bleiben `previewEnv.test.mjs` und `previewView.test.mjs` grün). `dotColor()` bleibt als Funktion bestehen, wird aber nicht mehr gerendert (kann in P10 samt Test entfallen).
|
||||
- `btn-sm`-Altlast (7 Dateien) verschwindet.
|
||||
- **Test-Auswirkung:** keine (Tests prüfen nur Logik). `servicesView.test.mjs` deepEqual-t `features` – nur ändern, wenn `servicesView.js` ein Feld bekommt (Standard: nicht).
|
||||
- **Sichtbar:** Projektbereich als Tabelle, Zuordnen als ruhige Schrittseite, flache Projekt-Abschnitte im Ticket und beim Kunden.
|
||||
|
||||
### P6: Chat, Gutschriften, Abo-Angebote, E-Mail-Speicher (4 PT)
|
||||
|
||||
Im Prototyp sind diese vier Seiten nur Platzhalter, die Gestaltung wird aus den Bausteinen abgeleitet und am ersten fertigen Screenshot freigegeben.
|
||||
|
||||
- **Dateien:**
|
||||
- Chat: `CustomerChatPage.jsx` (Liste als Haarlinien-Zeilen ohne Card, senkrechte Trennlinie `--line`, Ungelesen-Zähler als kleine Zahl oder schwarze Pille, `.input` für die Suche), `ChatThread.jsx` (nur Fehlerzeile ohne Icon, **`woms-chat`, `chat-messages` und die Container-Kinder bleiben**), `chat.css` komplett neu (siehe 7.2).
|
||||
- Gutschriften: `CreditsPage`, `CreditsView`, `CreditList` (Filter als `.tabs`, `.table` mit klickbarer Zeile, 7 Spalten brauchen `hide-sm/hide-md`-Priorisierung), `CreditDetail` (zweispaltig: `.two-col` + `h2.section` + `.kv`, Berechnung als `.field-row`, Pruef-Badges ohne Glyphen; Knöpfe: Genehmigen `.btn-primary`, Rückfrage `.btn`, Ablehnen `.link-danger`; `busyRef`, `window.confirm`, Debounce und die Eingabe-IDs `calc-*`/`credit-*` bleiben).
|
||||
- Abo-Angebote: `OffersPage`, `OffersView`, `OfferList` (Tabs/Select statt `.seg`, `.table`, Aktionen als `.link`), `OfferForm` (Dialog, Autocomplete-Clipping prüfen).
|
||||
- E-Mail-Speicher: `StorageTasksPage` (`.table`, `data-storage-done` am Knopf erhalten).
|
||||
- `admin-areas.css` fertig (Rest-Raster) bzw. löschen, sobald alle 7 Importeure umgestellt sind.
|
||||
- **Test-Auswirkung:** keine (reine Lib-Tests).
|
||||
- **Sichtbar:** Messenger hell, Gutschriften/Angebote/Speicher als Tabellen.
|
||||
|
||||
### P7: Finanzen (4-4,5 PT)
|
||||
|
||||
- **Dateien:**
|
||||
- `global.css` L1155-1476 neu (rund 320 Zeilen, 26 Dunkel-Literale).
|
||||
- `FinancePage` (`h2.section` je Block, **`INVOICES_ANCHOR` und `finanzen-buchungen` als `id` samt `scroll-margin-top` bleiben**, `.fin-stack gap` auf 0 setzen, sonst 36px + 24px Abstand), `FinanceSettingsPage`, `BankCallbackPage`.
|
||||
- `LiquidityHead` (`.stats` mit `.v.neg`), `StatusLine` (unboxed), `TodoList` (Zeilen mit Badge, kein farbiger Streifen), `ForecastTable`, `TaxCard`, `AboCard`, `InvoiceTable` (Altersstreifen als `.stats`, Filter `.seg`, Status-Badges, Aktionen als Text), `TransactionTable`, `Pager` (`.table-foot`), `BankAccessCard`, `FixedCostsCard`, `SettingsCard`, `FormField` (Fehlerring statt Rahmenfarbe), `AddressWarning`, `CustomerInvoices`.
|
||||
- Dialoge: `AssignDialog` (Zweischritt-Bestätigung unverändert, Vorschläge als `.check`-Zeilen statt Karten, `useDialogFocus`, Esc-Listener, `useScrollIntoView`), `InvoiceCreateModal` (breiter Dialog, Positionsraster behält die Spalten, Autocomplete-Popover, Enter sendet nie).
|
||||
- Responsive: die Container-Query-Stapelung (@container 560/640/860) bleibt, `container-type` wandert vom `.fin-*-card` auf den Wrapper (`.fin-block`/`.fin-table-wrap`). Die Klassen-Hooks (`fin-forecast-date`, `fin-inv-*`, `fin-tx-*` und `data-label`) und die `role`-Attribute bleiben.
|
||||
- Beträge: `euro()`/`euroWhole()`/`signedEuro()` (U+2212) und tabular-nums bleiben. Negative Salden `--red-fg`, die Vorzeichen bleiben der eigentliche Träger der Bedeutung.
|
||||
- **Test-Auswirkung:** keine (finance-Tests sind Lib-Tests, deepEqual auf Töne bleibt, weil das Mapping in `Badge.jsx` sitzt).
|
||||
- **Sichtbar:** Finanzen ohne Karten, Dialoge weiß.
|
||||
|
||||
### P8: Rechtliches und Admin (2 PT)
|
||||
|
||||
- **Dateien:**
|
||||
- Rechtliches: `LegalPage` (Tabs), `LegalTextsTab` (Kartenraster → eine Spalte oder Tabelle, Versionen als Haarlinienzeilen ohne Kästen und gestrichelte Ränder, Aufklapp-Zeile wird echter `<button>`), `ProviderContractsTab` (`.table`, Dringlichkeit nur im Termin-Feld als Badge/Text, keine getönten Zeilen), `ContractForm` und `VersionForm` auf `<Dialog>` mit `useDialogFocus` (heute ohne Fokusfalle und Esc), `legalUi`, `global.css` L1122-1153.
|
||||
- Admin: `AdminPage.jsx` (Header, Tabelle `Mitarbeiterkonten`, Formular als Dialog, Zeilenaktionen als Text), `AdminPage.css` komplett neu schreiben (rund 350 von 416 Zeilen entfallen). **Behalten und neu einfärben:** `.credential*`, `.sr-only`, `.spin` (werden auch von `CopyableCredential` in `CustomerDetailPage`, `CustomerFunktionen` benutzt; sie wandern nach `global.css`).
|
||||
- **Test-Auswirkung:** keine (`legal.test.mjs` pinnt nur die Token `red|orange|normal|grey|missing`, die als Daten bleiben).
|
||||
- **Sichtbar:** Rechtliches und Admin ohne Karten.
|
||||
|
||||
### P9: Serverpuls (1-1,5 PT)
|
||||
|
||||
- **Dateien:** `ServerpulsPage.jsx`, `serverpuls.css`, `AppsTable.jsx`, `Chart.jsx`, `Sparkline.jsx`.
|
||||
- **Änderungen:**
|
||||
- Kopf: Emoji → Text-Badge. Alerts als `.list-row` mit Stufen-Badge (kein Linksstreifen). Kacheln als `.stats` mit Sparkline darunter. Deltas als Textfarbe (rot = gestiegen, grün = gesunken). Panels als `h2.section`. Pills als `.seg`.
|
||||
- Chart-Farben als Konstanten (Canvas kann kein `var()`), eine gemeinsame Konstante: Linie `#111111`, Füllung `rgba(17,17,17,.06)`, Spitze `#8a93a3` gestrichelt, Achsen `#5e6678`, Grid `#ebeef1` (nur horizontal), Ticks `#d8dde3`. Die Projekt-Detail-Charts werden alle `#111111`. Legende 13px `--text-2`.
|
||||
- `data-testid` (19 Stück in Serverpuls plus `sp-nav-dot`) bleiben.
|
||||
- **Test-Auswirkung:** keine (`serverpulsFormat.test.mjs` prüft `dir` und `stale`, nicht Klassen).
|
||||
- **Sichtbar:** helles Dashboard.
|
||||
|
||||
### P10: Aufräumen (1 PT)
|
||||
|
||||
- **Dateien:**
|
||||
- `package.json` plus Lock: `@tabler/icons-react` und `react-icons` entfernen (nach `rg` = 0 Treffer).
|
||||
- Kompat-Block, tote Klassen und Aliase löschen (`.ui-btn*`, `.ui-card*`, `.badge-*`, `.overlay-content`, `.overlay-close`, `.form-control`/`.form-group`/`.form-label`, `.spinner`, `.btn-dark`, `.btn-teal`, `.text-grey`, `.dropdown*`, Alt-Token, `--surface-*` usw.).
|
||||
- `src/assets/fonts` (falls nicht schon in P1), `previewEnv.dotColor` samt Test, `Kpi`-Reste.
|
||||
- Kommentare in `navLinks.js` und `navLinks.test.mjs` ("Navbar ordnet Icons zu") anpassen.
|
||||
- **Optional, als eigenes kleines Paket:** `git rm -r --cached node_modules` (8.890 Dateien). `.gitignore` führt es ohnehin, der Build installiert neu, `tp2` ist unkritisch (siehe 7.6).
|
||||
- **Abschluss:** Bundle-Größe vorher/nachher, Screens aller Bereiche, `rg "style=\{"` dokumentieren (Ziel: nur Laufzeitwerte).
|
||||
- **Sichtbar:** nichts Neues, kleineres Bundle.
|
||||
|
||||
---
|
||||
|
||||
## 6. Offene Entscheidung: Ticket als eigene Seite oder aufklappen
|
||||
|
||||
Zusammenfassung der Vergleichsanalyse des Ticket-Lesers (Aufwand und Risiko jeweils auf den visuellen Umbau draufgerechnet):
|
||||
|
||||
| Kriterium | Eigene Seite (`/tickets/:woid`) | Aufklappen (Karte bleibt) |
|
||||
|---|---|---|
|
||||
| Abweichung vom freigegebenen Prototyp | keine | groß (Prototyp hat Crumb, Eyebrow, h1, Facts, Tabs mit URL) |
|
||||
| Zusatzaufwand Struktur | **+2 bis 2,5 PT** (Route+Seite+Hook 0,5; Karte → Seite, `handleCreateWorksheet` extrahieren 0,75; Filter in URL, Tabellenzeilen als Links 0,75; Deep-Links, Planboard, Navigation nach Neuanlage 0,25; QA 0,25-0,5) | **+0,75 bis 1 PT** (Zeile als Tabellenzeile mit Detail darunter, `<button aria-expanded>`, Auto-Aufklappen bei `?woid=`) |
|
||||
| Deep-Link auf ein Ticket oder einen Reiter | erstklassig (`/tickets/123/arbeitsblaetter`) | `?woid=` zeigt eine einzelne eingeklappte Karte, kein Reiter-Link |
|
||||
| Browser-Zurück und Lesezeichen | ja | nein |
|
||||
| Lange Listen | Liste bleibt kurz | Seite wird lang |
|
||||
| Performance | Liste mountet nicht mehr pro Zeile 3 Modals + `useEmployees` + `useWorksheets` + `useCustomerAbo` | bleibt pro Zeile |
|
||||
| Filterzustand | muss in die URL (löst nebenbei das heutige Zurücksetzen beim Zurückkehren) | entfällt |
|
||||
| Bekannter Fehler | Liste entlädt sich bei jedem Refetch → muss in beiden Varianten behoben werden | dort besonders schlimm: Refetch klappt alle offenen Karten zu und verliert deren Zustand |
|
||||
| Risiko | mittel: sechs bis sieben Links, Filter-in-URL, Tickets ohne `woid` (Fallback `$id.slice(-5)`), Zurück-Verhalten | niedrig |
|
||||
|
||||
**Empfehlung: eigene Seite.** Sie entspricht dem freigegebenen Prototyp und, laut Projektnotiz, der Entscheidung des Eigentümers (siehe 0.4). Außerdem verschwindet der teure Pro-Zeile-Zustand, und Deep-Links werden einfach. Die Mehrkosten von rund 1,5 PT gegenüber "aufklappen" bestehen größtenteils aus der Filter-in-URL-Arbeit, die ohnehin einen heutigen Schmerzpunkt behebt.
|
||||
|
||||
Umsetzungsskizze (alles in P2, kein Datenmodell nötig):
|
||||
1. Route `/tickets/:woid/:tab?` vor dem Fallback `*`. Die Sidebar markiert "Tickets" für Unterpfade bereits (`isActive` nutzt `startsWith(href + '/')`).
|
||||
2. Datenabruf per `woid` (nicht `$id`): `useWorkorder(woid)` mit `Query.equal('woid',[String(woid)])`, Rückgabe wie `useWorkorders` inklusive `updateWorkorder`. Zustände: Laden, Fehler, "Ticket nicht gefunden". Fallback für Tickets ohne `woid`: Parameter als `$id` zulassen (Direktabruf).
|
||||
3. Reiter `details`/`arbeitsblaetter` im Pfad (nicht im State), Modals einmal pro Seite.
|
||||
4. Legacy `?woid=` bleibt per `<Navigate replace>`. Die sieben Link-Stellen wechseln auf `ticketPath()`.
|
||||
5. Filter-, Such- und Limit-Zustand der Liste in `useSearchParams`; Breadcrumb "Tickets" führt mit demselben Suchstring zurück. Nach "Neues Ticket" Navigation auf `/tickets/<neue woid>` (`createWorkorder` liefert `data.woid`).
|
||||
6. `TicketProjects` löst bei jedem Seitenaufruf `importNewReposThrottled()` aus (auf 60 s gedrosselt). Das ist heute beim Aufklappen der Fall und ändert sich nicht.
|
||||
7. Fallback bei Rückzug der Entscheidung: P2 ohne Route (0,75-1 PT statt 2-2,5 PT), Zeile bleibt `<button aria-expanded>`, das Refetch-Entladen muss trotzdem behoben werden.
|
||||
|
||||
---
|
||||
|
||||
## 7. Risiken und Stolpersteine
|
||||
|
||||
### 7.1 Charts (Serverpuls, uPlot)
|
||||
- Canvas-Farben kommen als JS-Optionen (`Chart.jsx` L28-36 und L45, `ServerpulsPage.jsx` L223-237). CSS-Variablen wirken dort nicht, es müssen Literale sein. Die Instanz wird nur bei geänderten Serien-Labels oder Höhe neu gebaut, die Farben müssen also statische Konstanten sein.
|
||||
- Weiße Alpha-Grid- und Tick-Linien sind auf Weiß unsichtbar, `#9aa7b4` hat 2,45:1. Beides steht als Pflicht-Mini-Fix in P1.
|
||||
- Alt-Inkonsistenz: Amber ist im Hauptchart "Spitze", im Detailchart "CPU"; Smaragd ist im Hauptchart "CPU", im Detailchart "RAM". Mit durchgängigem Schwarz verschwindet das Problem.
|
||||
- `uplot/dist/uPlot.min.css` (Cursor `#607D8B`, Selektion `rgba(0,0,0,.07)`) passt schon auf Weiß.
|
||||
|
||||
### 7.2 Fremd gerenderter Chat-Composer
|
||||
- Das Portal-CSS `chat-composer.css` wird erst beim ersten Öffnen eines Chats ans Ende von `<head>` gehängt (`lib/loadChatComposer.js`), also **nach** dem gebauten WOMS-CSS. Gleich spezifische WOMS-Regeln verlieren. Deshalb müssen alle WOMS-Chat-Regeln das Präfix `.woms-chat` behalten oder `!important` tragen.
|
||||
- Der Fremdcode ist nur über `--chat-*`-Variablen auf `.woms-chat` und überstimmende Selektoren steuerbar. Hart verdrahtete Weiß- und Schwarz-Alphas (Icon-Knöpfe, Datei-Chips `rgba(0,0,0,.22)`, Tray, Audio-Leiste, Menü-Hover) sind auf Weiß unsichtbar oder nur halb lesbar. Sie müssen einzeln überschrieben werden: transparente Knöpfe mit Ring-Schatten, Chip `--bg-hover` ohne Rand, Menü weiß mit `--shadow-pop`, Senden-Knopf schwarz mit weißer Schrift, Fehlerfarben `--red-fg`.
|
||||
- Lightbox und Kamera hängen am `<body>` und sehen nur die Portal-Defaults (Lila, dunkel). Standard: dunkel lassen (Medienbetrachter), da sie die `--chat-*`-Werte von `.woms-chat` nicht erreichen.
|
||||
- Nicht änderbar aus WOMS: Texte ("Senden", "Aufnahme", "Bild", "Kamera", "Datei", "Dateien hier ablegen" als CSS-`content`), 15 Inline-SVGs, das Portal-Repo (teilt die Datei mit dem dunklen, lila Kundenportal). Eine Tokenisierung dort wäre ein Eingriff ins Kundenportal und nur nach Absprache sinnvoll.
|
||||
- Vertrag zwischen WOMS und Portal: Klassen `woms-chat`, `chat-messages`, `chat-bubble`, `chat-meta`, `chat-dropzone`/`chat-drop-active`, `dataset.stick`. React besitzt nur die leeren Container. `ChatThread.jsx` darf dort keine Kinder oder Struktur ergänzen.
|
||||
- Desktop-/Handy-Breakpoint des Fremdcodes ist 640 px (Platzhalter) und 880 px / coarse pointer (Senden-Symbol), unabhängig von den WOMS-Breakpoints. Der Chat-Wechsel Liste/Verlauf (`chat.css`, 720 px) bleibt davon unberührt.
|
||||
- Tipp: `tests/unit/loadChatComposer.test.mjs` sichert die Lade-Reihenfolge. Nicht anfassen.
|
||||
|
||||
### 7.3 Latin-1
|
||||
Nur `.env.development` und `.env.production`. Nicht editieren. Alle Quellen sind UTF-8, das Bearbeiten mit Edit/Write ist unproblematisch. Die einzige Binärdatei im Quellbaum ist `lato-regular.ttf` (wird gelöscht).
|
||||
|
||||
### 7.4 Tests
|
||||
- 312 Tests, reine Logik, keine Komponententests. Ein stilles Brechen von Markup oder Layout bemerkt kein Test, die Absicherung ist ausschließlich der Screenshot-Abgleich.
|
||||
- Design-gekoppelte Tests:
|
||||
- `adminRoute.test.mjs` liest `AdminRoute.jsx` als Quelltext und verlangt `<p>Wird geladen …</p>` ohne `className`/`style` (und kein "Loading"). Das Markup bleibt wörtlich.
|
||||
- `sidebar.test.mjs` (Breakpoint 720, `startsCollapsed`) wird mit `lib/sidebar.js` gelöscht.
|
||||
- `previewEnv.test.mjs` prüft die Hex-Werte von `dotColor()`, `previewView.test.mjs` die Token `green|yellow|red|grey`. **Gegenmittel:** Vokabular und Hex unverändert lassen, nur in der Komponente mappen.
|
||||
- Ton-Namen `muted|info|ok|warn|danger` sind deepEqual-gepinnt in `financeFormat`, `financeView`, `financeSettings`, `adminFormat`, `funktionenView`. Gegenmittel: Mapping nur in `Badge.jsx`.
|
||||
- `legal.test.mjs` pinnt `deadlineTone` (`red|orange|normal|grey|missing`).
|
||||
- `customerTabs.test.mjs` pinnt `Object.keys(entry)` auf `id` und `label`.
|
||||
- `navLinks.test.mjs`: deepEqual auf `key/label/href/count`, ein zusätzliches Feld `hot` ist erlaubt.
|
||||
- `servicesView.test.mjs` deepEqual-t `features [{label, statusLabel}]`.
|
||||
- Lücken (neu mit Tests): `ticketStatus.js` (Status-Töne), `ticketForm.worksheetTone`, `planboard`, alle Hooks, alle Komponenten, die extrahierte Worksheet-Logik, die URL-/Filter-Helfer.
|
||||
- `tests/unit/load.mjs` bündelt mit `esbuild`, das nur transitiv über `vite` da ist (0.21.5) und nicht in den `devDependencies` steht. Ein Vite-Major-Upgrade kann die Tests still brechen.
|
||||
|
||||
### 7.5 Paralleler Branch `feature/terminplaner-tp2`
|
||||
- Divergiert (1 ahead, 3 behind). Der eine Commit `6f03712` berührt `CustomerFunktionen.jsx` (3 Zeilen), `funktionenView.js`, `funktionenView.test.mjs`.
|
||||
- Empfehlung: **vor P4 in `test` mergen** (Push durch den Eigentümer) oder den Commit per Cherry-Pick in `dev-design` holen. Dann gibt es keinen Konflikt in der in P4 komplett neu gebauten Datei. Alternativ nach P4 per Rebase mit manuellem Auflösen der 3 Zeilen.
|
||||
- Sollte TP2 später weitere WOMS-Oberfläche bauen: ab P1 mit den neuen Klassen und `Dialog`, nicht mit den Altklassen. Der WOMS-Anteil des Plans ist mir nicht im Detail bekannt (der Plan-Commit `a72380c` liegt im Portal-Repo).
|
||||
|
||||
### 7.6 Getracktes `node_modules`
|
||||
- Siehe 0.6: für den Build wirkungslos, aber Stolperfalle für Commits (nur explizite Pfade) und für `npm uninstall` (147 gelöschte getrackte `react-icons`-Dateien erscheinen im `git status`).
|
||||
- Neue Worktrees checken die 8.890 Dateien mit aus und sind danach sauber (geprüft in `dev-termine`). Ein `npm install` im Worktree ändert getrackte Dateien.
|
||||
- Optionaler Cleanup `git rm -r --cached node_modules` ist unkritisch, auch wenn `tp2` das Verzeichnis unverändert trägt.
|
||||
|
||||
### 7.7 Deploy-Webhook und Auslieferung
|
||||
- Siehe 0.1 und 0.2. Nur `test` deployt, `build/` ist Live-Checkout, kein Staging.
|
||||
- Jedes Paket muss einzeln lauffähig und rückrollbar sein. Rollback per Revert-Commit.
|
||||
- Container-Build hat 1 GB Node-Heap und macht pro Deploy `npm install` (1-3 min). Neue `public/`-Dateien werden von Vite nach `dist/` kopiert, `deploy.sh` kopiert `dist/*` nach `/out`, `public/favicon.svg` ist damit unkritisch.
|
||||
- Server-Speicher knapp (Vite dev wird per OOM getötet). Nur der esbuild-Harness unter `systemd-run` ist belastbar.
|
||||
|
||||
### 7.8 Weitere Stolpersteine
|
||||
- **Eine CSS-Bundle:** `App.jsx` importiert alle Seiten statisch, es gibt genau ein CSS. Ein entfernter oder umbenannter Klassenname bricht Seiten, die niemand öffnet, und fällt erst zur Laufzeit auf. Das ist die Begründung für den Kompat-Block in P1 und die QA-Pflichtliste.
|
||||
- **Namenskollisionen** zwischen Prototyp und Bestand mit anderer Bedeutung: `.btn`, `.badge`, `.page`, `.field`, `.seg`, `.toolbar`, `.empty`, `.list-row`, `.urgent-toggle` (Schalter statt gestrichelter Pille), `.overlay` (Backdrop plus zentrierter `.dialog` statt schwarzer Vollbild), `.id`, `.muted`/`.faint`. `.table` ist unkritisch, die Bestandsregel `.table` ist tot.
|
||||
- **79 `!important`** (`.btn-green`, `.btn-primary`, `.form-control`, `.card`, `.status-*`, `.priority-*`, `.dropdown .btn`) kämpfen gegen neue Regeln. Löschen, nicht überschichten.
|
||||
- **Dokument-Scroll statt Scroll-Container:** `position: sticky` in `AdminPage.css` L186 bekommt einen neuen Bezug, `scrollIntoView` ist unkritisch (`useScrollIntoView`, `ChatThread` hat eine eigene Scrollbox). 100vh kommt nur in `LoginPage` und `global.css` L801 vor.
|
||||
- **Breakpoints:** Sidebar 720 → 860, dazwischen halbfertige Layouts, wenn Seiten noch die alten Breakpoints nutzen (`@media 720`, `700`, `640`, `768`). Pro Paket die Seitenbreakpoints auf 860/1100 ziehen.
|
||||
- **Dialoge:** `useDialogFocus` braucht `onKeyDown` am Overlay, einen inneren Dialog mit `ref` und `tabIndex={-1}` und fokussierbare Elemente **innerhalb** des Overlays (der Schließen-Button muss drin sein). Escape-Listener der Finanzdialoge: `InvoiceCreateModal` respektiert `e.defaultPrevented` für das Autocomplete. Native `required` und `novalidate`-Verhalten bleiben. Autocomplete-Popover (280px) im scrollenden `.dialog-body` kann abgeschnitten werden.
|
||||
- **Link-Erkennbarkeit:** viele Links hängen an `--accent` (siehe 2.2) und werden schwarz. `a { color: inherit }` plus `.link` mit Unterstreichung ist der Ausweg. `a.list-row` und Tabellenlinks brauchen eine Hover-Reaktion.
|
||||
- **Statusfarben-Semantik** (Offen grün → grau, Erledigt grau → grün, Prüfung teal → violett) ist für Mitarbeiter ein sichtbarer Bedeutungswechsel.
|
||||
- **Zeilenklick vs. Controls:** `tr.row` darf Klicks auf `a`, `button`, `select`, `input` nicht verschlucken (der Prototyp ignoriert sie). Für Tastatur und Mittelklick ein echter Link in der ersten Zelle.
|
||||
- **Abo-Stunden-Sichtbarkeitsregel** in `WorksheetList` (`onTogglePlan && !isComment && totalTime>0 && (aboHoursAllowed || countsToPlan)`) und `canEditTimes`, `canAddSupport`, `isGit`, `needsTimeEntry` bleiben beim Rewrite exakt.
|
||||
- **Datenabdeckung der Prototyp-Spalten:** "Zeit", "Tab-Zähler", "Offene Tickets (Kunden)", "Letzter Push" haben keine fertigen Felder (siehe Abschnitt 8).
|
||||
- **`tr`-Hover-Trick** des Prototyps (`box-shadow` links/rechts 12px) setzt eine `border-collapse: collapse`-Tabelle ohne `overflow:hidden` voraus. In Dialogen und Container-Wrappern prüfen.
|
||||
- **StrictMode:** doppelte Effekt-Läufe in Dev sind nach Entfernen von `LetterGlitch` (rAF-Dauerlast, keine Reduced-Motion-Abfrage) kein Thema mehr.
|
||||
|
||||
---
|
||||
|
||||
## 8. Was bewusst NICHT geändert wird
|
||||
|
||||
Das Redesign ist rein visuell. Ausnahme ist allein die Ticket-Seite samt Route (Abschnitt 6).
|
||||
|
||||
- **Verhalten:**
|
||||
- Alle Hooks (`src/hooks/*`) und alle Lib-Logik: `finance*`, `assign*`, `support`, `mfa`, `staffAccess`, `serverpuls*`, `projectList`, `analyse*`, `previewApi` usw.
|
||||
- Auth- und 2FA-Ablauf, Polling (Sidebar 5 min, Serverpuls 60 s plus bei Sichtbarkeit, Chat 15 s und 30 s, Preview 5 s), Pflichtfelder und Validierungen, 15-Minuten-Schritte, Doppelklick-Sperren (`busyRef`, `lockRef`).
|
||||
- Alle `window.confirm`/`alert`-Rückfragen bleiben (rund 12 Stellen).
|
||||
- URL-Parameter `?tab=`, `?antrag=`, `?kunde=`, `?woid=` (letzteres als Redirect), alle `data-testid`.
|
||||
- Navigation: `navLinks.js` (Reihenfolge, Labels, Hrefs, pausierte Leads) unverändert, Routen außer `/tickets/:woid`.
|
||||
- Lead-Automatik bleibt pausiert.
|
||||
- **Daten:**
|
||||
- Appwrite-Schema, Collections, Attribute, Permissions, Indizes. Keine neuen Felder, keine Datenmigration.
|
||||
- Gespeicherte Statuswerte (englisch, das Kundenportal leitet daraus die Fortschrittsleiste ab).
|
||||
- Portal-Repo und `chat-composer.css/js`.
|
||||
- `.env*` (Latin-1).
|
||||
- Server-API.
|
||||
- Sortierung der Ticketliste bleibt `$createdAt` absteigend. Der Prototyp-Fußtext "Sortiert nach Dringlichkeit, dann neueste" würde einen Index auf `priority` brauchen und wird deshalb im Fußtext angepasst, nicht in der Datenbank.
|
||||
- **Texte:**
|
||||
- Alle rechtlich relevanten Texte (AVV-/Zustimmungstext der Website-Analyse samt Textvorlage, Abo-/Gutschrift-/§19-Hinweise, `TaxCard`-Disclaimer), Mail- und Chat-Vorlagen und die Lib-Fehlermeldungen.
|
||||
- Statuslabels und Tab-Überschriften, soweit sie schon mit dem Prototyp übereinstimmen (Offen, In Arbeit, Wartet auf Kunde, Prüfung, Erledigt, Storniert).
|
||||
- Die ae/ue-Schreibung in Bestandstexten (zum Beispiel "oeffnen", "Menue") wird nicht flächig korrigiert.
|
||||
- Ausnahmen, die dem **Prototyp-Wortlaut** folgen (freigegeben): Login ("Anmelden", "E-Mail", "Passwort", "Weiter", "Code eingeben", Untertitel "Ticketsystem für das WEBklar-Team"), "Abmelden", "Wird geladen …" statt "Loading...", "Menü".
|
||||
- **Nicht im Umfang:** Dark-Mode-Umschalter, neue Funktionen, Terminplaner TP2, Server-/Compose-Änderungen.
|
||||
|
||||
### 8.1 Abweichungen Prototyp zu Bestand, die vor dem jeweiligen Paket entschieden werden müssen
|
||||
|
||||
Standardvorschlag in der letzten Spalte (wird übernommen, wenn der Eigentümer nichts anderes sagt).
|
||||
|
||||
| # | Paket | Punkt | Standard |
|
||||
|---|---|---|---|
|
||||
| 1 | P1/P2 | Statusfarben-Wechsel (Offen/Erledigt/Prüfung) | wie Prototyp |
|
||||
| 2 | P2 | Einfachwahl-Tabs (Aktiv, Offen, …) statt Mehrfachauswahl plus "Anwenden"; "Aktiv" = bisheriger Standard (open+working+waiting+review) | wie Prototyp, Filter live |
|
||||
| 3 | P2 | Tab-Zähler und Spalte "Zeit" (zusätzliche Lesezugriffe) | mitliefern (+0,5 PT), "Zuständig" aus Mitarbeiterliste |
|
||||
| 4 | P2 | Status-Auswahl: Prototyp bietet bei Akquise nur [In Kontakt, Erledigt, Storniert], Bestand zeigt für alle Tickets die sechs generischen Werte | Bestand beibehalten, aktueller Wert immer als Option |
|
||||
| 5 | P2 | "Neues Ticket": Prototyp mit Zuständig-Select und Einsatz-Segment, Bestand mit automatischer Zuweisung und ausgeblendetem Einsatz | Bestand beibehalten |
|
||||
| 6 | P2 | Arbeitsblatt-Dialog: Hinweis "Der Kunde sieht diesen Text im Kundenportal" ist im Prototyp, im Bestand unklar | nur übernehmen, wenn nachweislich wahr |
|
||||
| 7 | P2 | Reihenfolge der Arbeitsblätter (Prototyp älteste zuerst, Bestand neueste zuerst) | Bestand |
|
||||
| 8 | P2/P3 | Stornierte Tickets im Planboard | wie Bestand (anzeigen); Prototyp blendet sie aus |
|
||||
| 9 | P4 | Spalten "Telefon" und "Offene Tickets" in der Kundenliste | "Telefon" ja (Feld vorhanden), "Offene Tickets" nein (zusätzliche Abfrage) |
|
||||
| 10 | P4 | Stammdaten: Lese/Bearbeiten-Umschalter (Prototyp) oder immer editierbar (Bestand) | immer editierbar |
|
||||
| 11 | P5 | Projektansichten "Unzugeordnet", "Zugeordnet", "Alle inkl. Archiv" und Mitarbeiter-Filter haben im Prototyp keinen Platz | Tabs Aktiv/Vorlagen/Archiviert plus Selects (Kunde mit Option "Ohne Kunde", Mitarbeiter); "Alle" als vierter Tab |
|
||||
| 12 | P5 | Spalte "Letzter Push" (kein Feld in `websiteProjects`), "Art" meint `typeOf()` (Hosting-Modus), nicht `project.kind` (Services-Art) | `$updatedAt` zeigen oder weglassen; Begriffe sauber trennen |
|
||||
| 13 | P6/P7/P8/P9 | Chat, Gutschriften, Angebote, Speicher, Services, Finanzen, Rechtliches, Admin, Serverpuls sind im Prototyp nur Platzhalter | Gestaltung aus den Bausteinen, Abnahme am ersten fertigen Screenshot je Paket |
|
||||
| 14 | P6 | Chat-Blasen: eigene Blase grau oder schwarz | fremd `--bg-hover`, eigene `--grey-bg` (Schwarz wäre zu viel Akzent); Lightbox/Kamera dunkel |
|
||||
| 15 | P5 | Kopier-/Zeigen-Knöpfe in `CopyableCredential`: Icons oder Text | Text |
|
||||
| 16 | P1 | Footer ("Webklar Ticket-Plattform · © Jahr") | entfällt; falls nötig, 12px in `.sidebar-foot` |
|
||||
| 17 | P1 | `<title>` ("Webklar WOMS 2.0" oder "Ticketsystem · WEBklar") | unverändert lassen |
|
||||
| 18 | P1 | Serverpuls-Punkt: ein Zustand (Prototyp) oder zwei (Bestand: medium und critical) | zwei (amber, rot) |
|
||||
| 19 | P6 | Pseudo-Tabellen mit 5-7 Spalten (Gutschriften, Angebote): Prototyp blendet Spalten aus (`hide-sm/hide-md`) statt zu stapeln | Spaltenpriorität je Tabelle festlegen, Aktionen in letzte Spalte |
|
||||
| 20 | P2 | `.btn`-Variante "danger" und `size="sm"` haben im Ziel keine Entsprechung | `.link-danger` bzw. Einheitsgröße (32px) |
|
||||
+1
-1
@@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>Webklar WOMS 2.0</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Lato|Open+Sans" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
../baseline-browser-mapping/dist/cli.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../browserslist/cli.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../esbuild/bin/esbuild
|
||||
-1
@@ -1 +0,0 @@
|
||||
../jsesc/bin/jsesc
|
||||
-1
@@ -1 +0,0 @@
|
||||
../json5/lib/cli.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../loose-envify/cli.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../nanoid/bin/nanoid.cjs
|
||||
-1
@@ -1 +0,0 @@
|
||||
../@babel/parser/bin/babel-parser.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../rollup/dist/bin/rollup
|
||||
-1
@@ -1 +0,0 @@
|
||||
../semver/bin/semver.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../update-browserslist-db/cli.js
|
||||
-1
@@ -1 +0,0 @@
|
||||
../vite/bin/vite.js
|
||||
-1425
File diff suppressed because it is too large.
Load diff
-133
@@ -1,133 +0,0 @@
|
||||
{
|
||||
"hash": "d78f2739",
|
||||
"configHash": "b1cb6c91",
|
||||
"lockfileHash": "4b2b8d7d",
|
||||
"browserHash": "9a736d5a",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "2dd90c89",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "8229fa46",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "6db368d6",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "a8fbd3f7",
|
||||
"needsInterop": true
|
||||
},
|
||||
"appwrite": {
|
||||
"src": "../../appwrite/dist/esm/sdk.js",
|
||||
"file": "appwrite.js",
|
||||
"fileHash": "a2fa5c99",
|
||||
"needsInterop": false
|
||||
},
|
||||
"date-fns": {
|
||||
"src": "../../date-fns/esm/index.js",
|
||||
"file": "date-fns.js",
|
||||
"fileHash": "157aa30f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"date-fns/locale": {
|
||||
"src": "../../date-fns/esm/locale/index.js",
|
||||
"file": "date-fns_locale.js",
|
||||
"fileHash": "cd94d345",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "8948be38",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-icons/fa": {
|
||||
"src": "../../react-icons/fa/index.esm.js",
|
||||
"file": "react-icons_fa.js",
|
||||
"fileHash": "c064e92c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-icons/fa6": {
|
||||
"src": "../../react-icons/fa6/index.esm.js",
|
||||
"file": "react-icons_fa6.js",
|
||||
"fileHash": "0e64eca2",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.js",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "0004d718",
|
||||
"needsInterop": false
|
||||
},
|
||||
"motion/react": {
|
||||
"src": "../../motion/dist/es/react.mjs",
|
||||
"file": "motion_react.js",
|
||||
"fileHash": "42596b33",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@tabler/icons-react": {
|
||||
"src": "../../@tabler/icons-react/dist/esm/tabler-icons-react.mjs",
|
||||
"file": "@tabler_icons-react.js",
|
||||
"fileHash": "14b4db2f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"clsx": {
|
||||
"src": "../../clsx/dist/clsx.mjs",
|
||||
"file": "clsx.js",
|
||||
"fileHash": "87b6d9e4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"tailwind-merge": {
|
||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||
"file": "tailwind-merge.js",
|
||||
"fileHash": "adee159c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "f10da6a8",
|
||||
"needsInterop": false
|
||||
},
|
||||
"postprocessing": {
|
||||
"src": "../../postprocessing/build/index.js",
|
||||
"file": "postprocessing.js",
|
||||
"fileHash": "205bd067",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-DQJKJRV5": {
|
||||
"file": "chunk-DQJKJRV5.js"
|
||||
},
|
||||
"chunk-IFCYBMKG": {
|
||||
"file": "chunk-IFCYBMKG.js"
|
||||
},
|
||||
"chunk-6PXSGDAH": {
|
||||
"file": "chunk-6PXSGDAH.js"
|
||||
},
|
||||
"chunk-PJEEZAML": {
|
||||
"file": "chunk-PJEEZAML.js"
|
||||
},
|
||||
"chunk-DRWLMN53": {
|
||||
"file": "chunk-DRWLMN53.js"
|
||||
},
|
||||
"chunk-SJKHQ62W": {
|
||||
"file": "chunk-SJKHQ62W.js"
|
||||
},
|
||||
"chunk-G3PMV62Z": {
|
||||
"file": "chunk-G3PMV62Z.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
-3569
File diff suppressed because it is too large.
Load diff
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"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"],
|
||||
"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": ";;;;;;AAAA;AAAA;AAAA,WAAO,UAAU,OAAO;AAAA;AAAA;;;ACAxB;AAAA;AAAA,QAAI,SAAS,OAAO,SAAS,cAAc,OAAO;AAClD,QAAI,WAAY,WAAY;AAC5B,eAAS,IAAI;AACb,aAAK,QAAQ;AACb,aAAK,eAAe,OAAO;AAAA,MAC3B;AACA,QAAE,YAAY;AACd,aAAO,IAAI,EAAE;AAAA,IACb,EAAG;AACH,KAAC,SAASA,OAAM;AAEhB,UAAI,aAAc,SAAUC,UAAS;AAEnC,YAAI,UAAU;AAAA,UACZ,cAAc,qBAAqBD;AAAA,UACnC,UAAU,YAAYA,SAAQ,cAAc;AAAA,UAC5C,MACE,gBAAgBA,SAChB,UAAUA,SACT,WAAW;AACV,gBAAI;AACF,kBAAI,KAAK;AACT,qBAAO;AAAA,YACT,SAAS,GAAG;AACV,qBAAO;AAAA,YACT;AAAA,UACF,EAAG;AAAA,UACL,UAAU,cAAcA;AAAA,UACxB,aAAa,iBAAiBA;AAAA,QAChC;AAEA,iBAAS,WAAW,KAAK;AACvB,iBAAO,OAAO,SAAS,UAAU,cAAc,GAAG;AAAA,QACpD;AAEA,YAAI,QAAQ,aAAa;AACvB,cAAI,cAAc;AAAA,YAChB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAEA,cAAI,oBACF,YAAY,UACZ,SAAS,KAAK;AACZ,mBAAO,OAAO,YAAY,QAAQ,OAAO,UAAU,SAAS,KAAK,GAAG,CAAC,IAAI;AAAA,UAC3E;AAAA,QACJ;AAEA,iBAAS,cAAc,MAAM;AAC3B,cAAI,OAAO,SAAS,UAAU;AAC5B,mBAAO,OAAO,IAAI;AAAA,UACpB;AACA,cAAI,4BAA4B,KAAK,IAAI,GAAG;AAC1C,kBAAM,IAAI,UAAU,wCAAwC;AAAA,UAC9D;AACA,iBAAO,KAAK,YAAY;AAAA,QAC1B;AAEA,iBAAS,eAAe,OAAO;AAC7B,cAAI,OAAO,UAAU,UAAU;AAC7B,oBAAQ,OAAO,KAAK;AAAA,UACtB;AACA,iBAAO;AAAA,QACT;AAGA,iBAAS,YAAY,OAAO;AAC1B,cAAI,WAAW;AAAA,YACb,MAAM,WAAW;AACf,kBAAI,QAAQ,MAAM,MAAM;AACxB,qBAAO,EAAC,MAAM,UAAU,QAAW,MAAY;AAAA,YACjD;AAAA,UACF;AAEA,cAAI,QAAQ,UAAU;AACpB,qBAAS,OAAO,QAAQ,IAAI,WAAW;AACrC,qBAAO;AAAA,YACT;AAAA,UACF;AAEA,iBAAO;AAAA,QACT;AAEA,iBAAS,QAAQ,SAAS;AACxB,eAAK,MAAM,CAAC;AAEZ,cAAI,mBAAmB,SAAS;AAC9B,oBAAQ,QAAQ,SAAS,OAAO,MAAM;AACpC,mBAAK,OAAO,MAAM,KAAK;AAAA,YACzB,GAAG,IAAI;AAAA,UACT,WAAW,MAAM,QAAQ,OAAO,GAAG;AACjC,oBAAQ,QAAQ,SAAS,QAAQ;AAC/B,mBAAK,OAAO,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AAAA,YAClC,GAAG,IAAI;AAAA,UACT,WAAW,SAAS;AAClB,mBAAO,oBAAoB,OAAO,EAAE,QAAQ,SAAS,MAAM;AACzD,mBAAK,OAAO,MAAM,QAAQ,IAAI,CAAC;AAAA,YACjC,GAAG,IAAI;AAAA,UACT;AAAA,QACF;AAEA,gBAAQ,UAAU,SAAS,SAAS,MAAM,OAAO;AAC/C,iBAAO,cAAc,IAAI;AACzB,kBAAQ,eAAe,KAAK;AAC5B,cAAI,WAAW,KAAK,IAAI,IAAI;AAC5B,eAAK,IAAI,IAAI,IAAI,WAAW,WAAW,OAAO,QAAQ;AAAA,QACxD;AAEA,gBAAQ,UAAU,QAAQ,IAAI,SAAS,MAAM;AAC3C,iBAAO,KAAK,IAAI,cAAc,IAAI,CAAC;AAAA,QACrC;AAEA,gBAAQ,UAAU,MAAM,SAAS,MAAM;AACrC,iBAAO,cAAc,IAAI;AACzB,iBAAO,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI;AAAA,QAC3C;AAEA,gBAAQ,UAAU,MAAM,SAAS,MAAM;AACrC,iBAAO,KAAK,IAAI,eAAe,cAAc,IAAI,CAAC;AAAA,QACpD;AAEA,gBAAQ,UAAU,MAAM,SAAS,MAAM,OAAO;AAC5C,eAAK,IAAI,cAAc,IAAI,CAAC,IAAI,eAAe,KAAK;AAAA,QACtD;AAEA,gBAAQ,UAAU,UAAU,SAAS,UAAU,SAAS;AACtD,mBAAS,QAAQ,KAAK,KAAK;AACzB,gBAAI,KAAK,IAAI,eAAe,IAAI,GAAG;AACjC,uBAAS,KAAK,SAAS,KAAK,IAAI,IAAI,GAAG,MAAM,IAAI;AAAA,YACnD;AAAA,UACF;AAAA,QACF;AAEA,gBAAQ,UAAU,OAAO,WAAW;AAClC,cAAI,QAAQ,CAAC;AACb,eAAK,QAAQ,SAAS,OAAO,MAAM;AACjC,kBAAM,KAAK,IAAI;AAAA,UACjB,CAAC;AACD,iBAAO,YAAY,KAAK;AAAA,QAC1B;AAEA,gBAAQ,UAAU,SAAS,WAAW;AACpC,cAAI,QAAQ,CAAC;AACb,eAAK,QAAQ,SAAS,OAAO;AAC3B,kBAAM,KAAK,KAAK;AAAA,UAClB,CAAC;AACD,iBAAO,YAAY,KAAK;AAAA,QAC1B;AAEA,gBAAQ,UAAU,UAAU,WAAW;AACrC,cAAI,QAAQ,CAAC;AACb,eAAK,QAAQ,SAAS,OAAO,MAAM;AACjC,kBAAM,KAAK,CAAC,MAAM,KAAK,CAAC;AAAA,UAC1B,CAAC;AACD,iBAAO,YAAY,KAAK;AAAA,QAC1B;AAEA,YAAI,QAAQ,UAAU;AACpB,kBAAQ,UAAU,OAAO,QAAQ,IAAI,QAAQ,UAAU;AAAA,QACzD;AAEA,iBAAS,SAAS,MAAM;AACtB,cAAI,KAAK,UAAU;AACjB,mBAAO,QAAQ,OAAO,IAAI,UAAU,cAAc,CAAC;AAAA,UACrD;AACA,eAAK,WAAW;AAAA,QAClB;AAEA,iBAAS,gBAAgB,QAAQ;AAC/B,iBAAO,IAAI,QAAQ,SAAS,SAAS,QAAQ;AAC3C,mBAAO,SAAS,WAAW;AACzB,sBAAQ,OAAO,MAAM;AAAA,YACvB;AACA,mBAAO,UAAU,WAAW;AAC1B,qBAAO,OAAO,KAAK;AAAA,YACrB;AAAA,UACF,CAAC;AAAA,QACH;AAEA,iBAAS,sBAAsB,MAAM;AACnC,cAAI,SAAS,IAAI,WAAW;AAC5B,cAAI,UAAU,gBAAgB,MAAM;AACpC,iBAAO,kBAAkB,IAAI;AAC7B,iBAAO;AAAA,QACT;AAEA,iBAAS,eAAe,MAAM;AAC5B,cAAI,SAAS,IAAI,WAAW;AAC5B,cAAI,UAAU,gBAAgB,MAAM;AACpC,iBAAO,WAAW,IAAI;AACtB,iBAAO;AAAA,QACT;AAEA,iBAAS,sBAAsB,KAAK;AAClC,cAAI,OAAO,IAAI,WAAW,GAAG;AAC7B,cAAI,QAAQ,IAAI,MAAM,KAAK,MAAM;AAEjC,mBAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,kBAAM,CAAC,IAAI,OAAO,aAAa,KAAK,CAAC,CAAC;AAAA,UACxC;AACA,iBAAO,MAAM,KAAK,EAAE;AAAA,QACtB;AAEA,iBAAS,YAAY,KAAK;AACxB,cAAI,IAAI,OAAO;AACb,mBAAO,IAAI,MAAM,CAAC;AAAA,UACpB,OAAO;AACL,gBAAI,OAAO,IAAI,WAAW,IAAI,UAAU;AACxC,iBAAK,IAAI,IAAI,WAAW,GAAG,CAAC;AAC5B,mBAAO,KAAK;AAAA,UACd;AAAA,QACF;AAEA,iBAAS,OAAO;AACd,eAAK,WAAW;AAEhB,eAAK,YAAY,SAAS,MAAM;AAC9B,iBAAK,YAAY;AACjB,gBAAI,CAAC,MAAM;AACT,mBAAK,YAAY;AAAA,YACnB,WAAW,OAAO,SAAS,UAAU;AACnC,mBAAK,YAAY;AAAA,YACnB,WAAW,QAAQ,QAAQ,KAAK,UAAU,cAAc,IAAI,GAAG;AAC7D,mBAAK,YAAY;AAAA,YACnB,WAAW,QAAQ,YAAY,SAAS,UAAU,cAAc,IAAI,GAAG;AACrE,mBAAK,gBAAgB;AAAA,YACvB,WAAW,QAAQ,gBAAgB,gBAAgB,UAAU,cAAc,IAAI,GAAG;AAChF,mBAAK,YAAY,KAAK,SAAS;AAAA,YACjC,WAAW,QAAQ,eAAe,QAAQ,QAAQ,WAAW,IAAI,GAAG;AAClE,mBAAK,mBAAmB,YAAY,KAAK,MAAM;AAE/C,mBAAK,YAAY,IAAI,KAAK,CAAC,KAAK,gBAAgB,CAAC;AAAA,YACnD,WAAW,QAAQ,gBAAgB,YAAY,UAAU,cAAc,IAAI,KAAK,kBAAkB,IAAI,IAAI;AACxG,mBAAK,mBAAmB,YAAY,IAAI;AAAA,YAC1C,OAAO;AACL,mBAAK,YAAY,OAAO,OAAO,UAAU,SAAS,KAAK,IAAI;AAAA,YAC7D;AAEA,gBAAI,CAAC,KAAK,QAAQ,IAAI,cAAc,GAAG;AACrC,kBAAI,OAAO,SAAS,UAAU;AAC5B,qBAAK,QAAQ,IAAI,gBAAgB,0BAA0B;AAAA,cAC7D,WAAW,KAAK,aAAa,KAAK,UAAU,MAAM;AAChD,qBAAK,QAAQ,IAAI,gBAAgB,KAAK,UAAU,IAAI;AAAA,cACtD,WAAW,QAAQ,gBAAgB,gBAAgB,UAAU,cAAc,IAAI,GAAG;AAChF,qBAAKLine truncated
|
||||
"names": ["self", "exports", "fetch", "_b", "_a"]
|
||||
}
|
||||
-520
@@ -1,520 +0,0 @@
|
||||
import {
|
||||
add,
|
||||
addBusinessDays,
|
||||
addDays,
|
||||
addHours,
|
||||
addISOWeekYears,
|
||||
addMilliseconds,
|
||||
addMinutes,
|
||||
addMonths,
|
||||
addQuarters,
|
||||
addSeconds,
|
||||
addWeeks,
|
||||
addYears,
|
||||
areIntervalsOverlapping,
|
||||
clamp,
|
||||
closestIndexTo,
|
||||
closestTo,
|
||||
compareAsc,
|
||||
compareDesc,
|
||||
daysInWeek,
|
||||
daysInYear,
|
||||
daysToWeeks,
|
||||
differenceInBusinessDays,
|
||||
differenceInCalendarDays,
|
||||
differenceInCalendarISOWeekYears,
|
||||
differenceInCalendarISOWeeks,
|
||||
differenceInCalendarMonths,
|
||||
differenceInCalendarQuarters,
|
||||
differenceInCalendarWeeks,
|
||||
differenceInCalendarYears,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
differenceInISOWeekYears,
|
||||
differenceInMilliseconds,
|
||||
differenceInMinutes,
|
||||
differenceInMonths,
|
||||
differenceInQuarters,
|
||||
differenceInSeconds,
|
||||
differenceInWeeks,
|
||||
differenceInYears,
|
||||
eachDayOfInterval,
|
||||
eachHourOfInterval,
|
||||
eachMinuteOfInterval,
|
||||
eachMonthOfInterval,
|
||||
eachQuarterOfInterval,
|
||||
eachWeekOfInterval,
|
||||
eachWeekendOfInterval,
|
||||
eachWeekendOfMonth,
|
||||
eachWeekendOfYear,
|
||||
eachYearOfInterval,
|
||||
endOfDay,
|
||||
endOfDecade,
|
||||
endOfHour,
|
||||
endOfISOWeek,
|
||||
endOfISOWeekYear,
|
||||
endOfMinute,
|
||||
endOfMonth,
|
||||
endOfQuarter,
|
||||
endOfSecond,
|
||||
endOfToday,
|
||||
endOfTomorrow,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
endOfYesterday,
|
||||
format,
|
||||
formatDistance,
|
||||
formatDistanceStrict,
|
||||
formatDistanceToNow,
|
||||
formatDistanceToNowStrict,
|
||||
formatDuration,
|
||||
formatISO,
|
||||
formatISO9075,
|
||||
formatISODuration,
|
||||
formatRFC3339,
|
||||
formatRFC7231,
|
||||
formatRelative,
|
||||
fromUnixTime,
|
||||
getDate,
|
||||
getDay,
|
||||
getDayOfYear,
|
||||
getDaysInMonth,
|
||||
getDaysInYear,
|
||||
getDecade,
|
||||
getDefaultOptions,
|
||||
getHours,
|
||||
getISODay,
|
||||
getISOWeek,
|
||||
getISOWeekYear,
|
||||
getISOWeeksInYear,
|
||||
getMilliseconds,
|
||||
getMinutes,
|
||||
getMonth,
|
||||
getOverlappingDaysInIntervals,
|
||||
getQuarter,
|
||||
getSeconds,
|
||||
getTime,
|
||||
getUnixTime,
|
||||
getWeek,
|
||||
getWeekOfMonth,
|
||||
getWeekYear,
|
||||
getWeeksInMonth,
|
||||
getYear,
|
||||
hoursToMilliseconds,
|
||||
hoursToMinutes,
|
||||
hoursToSeconds,
|
||||
intervalToDuration,
|
||||
intlFormat,
|
||||
intlFormatDistance,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isDate,
|
||||
isEqual,
|
||||
isExists,
|
||||
isFirstDayOfMonth,
|
||||
isFriday,
|
||||
isFuture,
|
||||
isLastDayOfMonth,
|
||||
isLeapYear,
|
||||
isMatch,
|
||||
isMonday,
|
||||
isPast,
|
||||
isSameDay,
|
||||
isSameHour,
|
||||
isSameISOWeek,
|
||||
isSameISOWeekYear,
|
||||
isSameMinute,
|
||||
isSameMonth,
|
||||
isSameQuarter,
|
||||
isSameSecond,
|
||||
isSameWeek,
|
||||
isSameYear,
|
||||
isSaturday,
|
||||
isSunday,
|
||||
isThisHour,
|
||||
isThisISOWeek,
|
||||
isThisMinute,
|
||||
isThisMonth,
|
||||
isThisQuarter,
|
||||
isThisSecond,
|
||||
isThisWeek,
|
||||
isThisYear,
|
||||
isThursday,
|
||||
isToday,
|
||||
isTomorrow,
|
||||
isTuesday,
|
||||
isValid,
|
||||
isWednesday,
|
||||
isWeekend,
|
||||
isWithinInterval,
|
||||
isYesterday,
|
||||
lastDayOfDecade,
|
||||
lastDayOfISOWeek,
|
||||
lastDayOfISOWeekYear,
|
||||
lastDayOfMonth,
|
||||
lastDayOfQuarter,
|
||||
lastDayOfWeek,
|
||||
lastDayOfYear,
|
||||
lightFormat,
|
||||
max,
|
||||
maxTime,
|
||||
milliseconds,
|
||||
millisecondsInHour,
|
||||
millisecondsInMinute,
|
||||
millisecondsInSecond,
|
||||
millisecondsToHours,
|
||||
millisecondsToMinutes,
|
||||
millisecondsToSeconds,
|
||||
min,
|
||||
minTime,
|
||||
minutesInHour,
|
||||
minutesToHours,
|
||||
minutesToMilliseconds,
|
||||
minutesToSeconds,
|
||||
monthsInQuarter,
|
||||
monthsInYear,
|
||||
monthsToQuarters,
|
||||
monthsToYears,
|
||||
nextDay,
|
||||
nextFriday,
|
||||
nextMonday,
|
||||
nextSaturday,
|
||||
nextSunday,
|
||||
nextThursday,
|
||||
nextTuesday,
|
||||
nextWednesday,
|
||||
parse,
|
||||
parseISO,
|
||||
parseJSON,
|
||||
previousDay,
|
||||
previousFriday,
|
||||
previousMonday,
|
||||
previousSaturday,
|
||||
previousSunday,
|
||||
previousThursday,
|
||||
previousTuesday,
|
||||
previousWednesday,
|
||||
quartersInYear,
|
||||
quartersToMonths,
|
||||
quartersToYears,
|
||||
roundToNearestMinutes,
|
||||
secondsInDay,
|
||||
secondsInHour,
|
||||
secondsInMinute,
|
||||
secondsInMonth,
|
||||
secondsInQuarter,
|
||||
secondsInWeek,
|
||||
secondsInYear,
|
||||
secondsToHours,
|
||||
secondsToMilliseconds,
|
||||
secondsToMinutes,
|
||||
set,
|
||||
setDate,
|
||||
setDay,
|
||||
setDayOfYear,
|
||||
setDefaultOptions,
|
||||
setHours,
|
||||
setISODay,
|
||||
setISOWeek,
|
||||
setISOWeekYear,
|
||||
setMilliseconds,
|
||||
setMinutes,
|
||||
setMonth,
|
||||
setQuarter,
|
||||
setSeconds,
|
||||
setWeek,
|
||||
setWeekYear,
|
||||
setYear,
|
||||
startOfDay,
|
||||
startOfDecade,
|
||||
startOfHour,
|
||||
startOfISOWeek,
|
||||
startOfISOWeekYear,
|
||||
startOfMinute,
|
||||
startOfMonth,
|
||||
startOfQuarter,
|
||||
startOfSecond,
|
||||
startOfToday,
|
||||
startOfTomorrow,
|
||||
startOfWeek,
|
||||
startOfWeekYear,
|
||||
startOfYear,
|
||||
startOfYesterday,
|
||||
sub,
|
||||
subBusinessDays,
|
||||
subDays,
|
||||
subHours,
|
||||
subISOWeekYears,
|
||||
subMilliseconds,
|
||||
subMinutes,
|
||||
subMonths,
|
||||
subQuarters,
|
||||
subSeconds,
|
||||
subWeeks,
|
||||
subYears,
|
||||
toDate,
|
||||
weeksToDays,
|
||||
yearsToMonths,
|
||||
yearsToQuarters
|
||||
} from "./chunk-SJKHQ62W.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
add,
|
||||
addBusinessDays,
|
||||
addDays,
|
||||
addHours,
|
||||
addISOWeekYears,
|
||||
addMilliseconds,
|
||||
addMinutes,
|
||||
addMonths,
|
||||
addQuarters,
|
||||
addSeconds,
|
||||
addWeeks,
|
||||
addYears,
|
||||
areIntervalsOverlapping,
|
||||
clamp,
|
||||
closestIndexTo,
|
||||
closestTo,
|
||||
compareAsc,
|
||||
compareDesc,
|
||||
daysInWeek,
|
||||
daysInYear,
|
||||
daysToWeeks,
|
||||
differenceInBusinessDays,
|
||||
differenceInCalendarDays,
|
||||
differenceInCalendarISOWeekYears,
|
||||
differenceInCalendarISOWeeks,
|
||||
differenceInCalendarMonths,
|
||||
differenceInCalendarQuarters,
|
||||
differenceInCalendarWeeks,
|
||||
differenceInCalendarYears,
|
||||
differenceInDays,
|
||||
differenceInHours,
|
||||
differenceInISOWeekYears,
|
||||
differenceInMilliseconds,
|
||||
differenceInMinutes,
|
||||
differenceInMonths,
|
||||
differenceInQuarters,
|
||||
differenceInSeconds,
|
||||
differenceInWeeks,
|
||||
differenceInYears,
|
||||
eachDayOfInterval,
|
||||
eachHourOfInterval,
|
||||
eachMinuteOfInterval,
|
||||
eachMonthOfInterval,
|
||||
eachQuarterOfInterval,
|
||||
eachWeekOfInterval,
|
||||
eachWeekendOfInterval,
|
||||
eachWeekendOfMonth,
|
||||
eachWeekendOfYear,
|
||||
eachYearOfInterval,
|
||||
endOfDay,
|
||||
endOfDecade,
|
||||
endOfHour,
|
||||
endOfISOWeek,
|
||||
endOfISOWeekYear,
|
||||
endOfMinute,
|
||||
endOfMonth,
|
||||
endOfQuarter,
|
||||
endOfSecond,
|
||||
endOfToday,
|
||||
endOfTomorrow,
|
||||
endOfWeek,
|
||||
endOfYear,
|
||||
endOfYesterday,
|
||||
format,
|
||||
formatDistance,
|
||||
formatDistanceStrict,
|
||||
formatDistanceToNow,
|
||||
formatDistanceToNowStrict,
|
||||
formatDuration,
|
||||
formatISO,
|
||||
formatISO9075,
|
||||
formatISODuration,
|
||||
formatRFC3339,
|
||||
formatRFC7231,
|
||||
formatRelative,
|
||||
fromUnixTime,
|
||||
getDate,
|
||||
getDay,
|
||||
getDayOfYear,
|
||||
getDaysInMonth,
|
||||
getDaysInYear,
|
||||
getDecade,
|
||||
getDefaultOptions,
|
||||
getHours,
|
||||
getISODay,
|
||||
getISOWeek,
|
||||
getISOWeekYear,
|
||||
getISOWeeksInYear,
|
||||
getMilliseconds,
|
||||
getMinutes,
|
||||
getMonth,
|
||||
getOverlappingDaysInIntervals,
|
||||
getQuarter,
|
||||
getSeconds,
|
||||
getTime,
|
||||
getUnixTime,
|
||||
getWeek,
|
||||
getWeekOfMonth,
|
||||
getWeekYear,
|
||||
getWeeksInMonth,
|
||||
getYear,
|
||||
hoursToMilliseconds,
|
||||
hoursToMinutes,
|
||||
hoursToSeconds,
|
||||
intervalToDuration,
|
||||
intlFormat,
|
||||
intlFormatDistance,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isDate,
|
||||
isEqual,
|
||||
isExists,
|
||||
isFirstDayOfMonth,
|
||||
isFriday,
|
||||
isFuture,
|
||||
isLastDayOfMonth,
|
||||
isLeapYear,
|
||||
isMatch,
|
||||
isMonday,
|
||||
isPast,
|
||||
isSameDay,
|
||||
isSameHour,
|
||||
isSameISOWeek,
|
||||
isSameISOWeekYear,
|
||||
isSameMinute,
|
||||
isSameMonth,
|
||||
isSameQuarter,
|
||||
isSameSecond,
|
||||
isSameWeek,
|
||||
isSameYear,
|
||||
isSaturday,
|
||||
isSunday,
|
||||
isThisHour,
|
||||
isThisISOWeek,
|
||||
isThisMinute,
|
||||
isThisMonth,
|
||||
isThisQuarter,
|
||||
isThisSecond,
|
||||
isThisWeek,
|
||||
isThisYear,
|
||||
isThursday,
|
||||
isToday,
|
||||
isTomorrow,
|
||||
isTuesday,
|
||||
isValid,
|
||||
isWednesday,
|
||||
isWeekend,
|
||||
isWithinInterval,
|
||||
isYesterday,
|
||||
lastDayOfDecade,
|
||||
lastDayOfISOWeek,
|
||||
lastDayOfISOWeekYear,
|
||||
lastDayOfMonth,
|
||||
lastDayOfQuarter,
|
||||
lastDayOfWeek,
|
||||
lastDayOfYear,
|
||||
lightFormat,
|
||||
max,
|
||||
maxTime,
|
||||
milliseconds,
|
||||
millisecondsInHour,
|
||||
millisecondsInMinute,
|
||||
millisecondsInSecond,
|
||||
millisecondsToHours,
|
||||
millisecondsToMinutes,
|
||||
millisecondsToSeconds,
|
||||
min,
|
||||
minTime,
|
||||
minutesInHour,
|
||||
minutesToHours,
|
||||
minutesToMilliseconds,
|
||||
minutesToSeconds,
|
||||
monthsInQuarter,
|
||||
monthsInYear,
|
||||
monthsToQuarters,
|
||||
monthsToYears,
|
||||
nextDay,
|
||||
nextFriday,
|
||||
nextMonday,
|
||||
nextSaturday,
|
||||
nextSunday,
|
||||
nextThursday,
|
||||
nextTuesday,
|
||||
nextWednesday,
|
||||
parse,
|
||||
parseISO,
|
||||
parseJSON,
|
||||
previousDay,
|
||||
previousFriday,
|
||||
previousMonday,
|
||||
previousSaturday,
|
||||
previousSunday,
|
||||
previousThursday,
|
||||
previousTuesday,
|
||||
previousWednesday,
|
||||
quartersInYear,
|
||||
quartersToMonths,
|
||||
quartersToYears,
|
||||
roundToNearestMinutes,
|
||||
secondsInDay,
|
||||
secondsInHour,
|
||||
secondsInMinute,
|
||||
secondsInMonth,
|
||||
secondsInQuarter,
|
||||
secondsInWeek,
|
||||
secondsInYear,
|
||||
secondsToHours,
|
||||
secondsToMilliseconds,
|
||||
secondsToMinutes,
|
||||
set,
|
||||
setDate,
|
||||
setDay,
|
||||
setDayOfYear,
|
||||
setDefaultOptions,
|
||||
setHours,
|
||||
setISODay,
|
||||
setISOWeek,
|
||||
setISOWeekYear,
|
||||
setMilliseconds,
|
||||
setMinutes,
|
||||
setMonth,
|
||||
setQuarter,
|
||||
setSeconds,
|
||||
setWeek,
|
||||
setWeekYear,
|
||||
setYear,
|
||||
startOfDay,
|
||||
startOfDecade,
|
||||
startOfHour,
|
||||
startOfISOWeek,
|
||||
startOfISOWeekYear,
|
||||
startOfMinute,
|
||||
startOfMonth,
|
||||
startOfQuarter,
|
||||
startOfSecond,
|
||||
startOfToday,
|
||||
startOfTomorrow,
|
||||
startOfWeek,
|
||||
startOfWeekYear,
|
||||
startOfYear,
|
||||
startOfYesterday,
|
||||
sub,
|
||||
subBusinessDays,
|
||||
subDays,
|
||||
subHours,
|
||||
subISOWeekYears,
|
||||
subMilliseconds,
|
||||
subMinutes,
|
||||
subMonths,
|
||||
subQuarters,
|
||||
subSeconds,
|
||||
subWeeks,
|
||||
subYears,
|
||||
toDate,
|
||||
weeksToDays,
|
||||
yearsToMonths,
|
||||
yearsToQuarters
|
||||
};
|
||||
//# sourceMappingURL=date-fns.js.map
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
-34321
File diff suppressed because it is too large.
Load diff
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"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
|
||||
"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": ";;;;;;;;;;;;;;;;;AAAA,IAAI,uBAAuB;AAAA,EACzB,kBAAkB;AAAA,IAChB,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,aAAa;AAAA,EACb,kBAAkB;AAAA,IAChB,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,aAAa;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,QAAQ;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,OAAO;AAAA,IACL,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,aAAa;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,QAAQ;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,cAAc;AAAA,IACZ,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,SAAS;AAAA,IACP,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,aAAa;AAAA,IACX,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,QAAQ;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,YAAY;AAAA,IACV,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AAAA,EACA,cAAc;AAAA,IACZ,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AACF;AACA,IAAI,iBAAiB,SAASA,gBAAe,OAAO,OAAO,SAAS;AAClE,MAAI;AACJ,MAAI,aAAa,qBAAqB,KAAK;AAC3C,MAAI,OAAO,eAAe,UAAU;AAClC,aAAS;AAAA,EACX,WAAW,UAAU,GAAG;AACtB,aAAS,WAAW;AAAA,EACtB,OAAO;AACL,aAAS,WAAW,MAAM,QAAQ,aAAa,OAAO,KAAK,CAAC;AAAA,EAC9D;AACA,MAAI,YAAY,QAAQ,YAAY,UAAU,QAAQ,WAAW;AAC/D,QAAI,QAAQ,cAAc,QAAQ,aAAa,GAAG;AAChD,aAAO,SAAS;AAAA,IAClB,OAAO;AACL,aAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACA,SAAO;AACT;AACA,IAAOC,0BAAQ;;;ACjFf,IAAI,cAAc;AAAA,EAChB,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AACT;AACA,IAAI,cAAc;AAAA,EAChB,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AACT;AACA,IAAI,kBAAkB;AAAA,EACpB,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AACT;AACA,IAAI,aAAa;AAAA,EACf,MAAM,kBAAkB;AAAA,IACtB,SAAS;AAAA,IACT,cAAc;AAAA,EAChB,CAAC;AAAA,EACD,MAAM,kBAAkB;AAAA,IACtB,SAAS;AAAA,IACT,cAAc;AAAA,EAChB,CAAC;AAAA,EACD,UAAU,kBAAkB;AAAA,IAC1B,SAAS;AAAA,IACT,cAAc;AAAA,EAChB,CAAC;AACH;AACA,IAAO,qBAAQ;;;ACjCf,IAAI,uBAAuB;AAAA,EACzB,UAAU;AAAA,EACV,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AACT;AACA,IAAI,iBAAiB,SAASC,gBAAe,OAAO,OAAO,WAAW,UAAU;AAC9E,SAAO,qBAAqB,KAAK;AACnC;AACA,IAAOC,0BAAQ;;;ACVf,IAAI,YAAY;AAAA,EACd,QAAQ,CAAC,MAAM,IAAI;AAAA,EACnB,aAAa,CAAC,MAAM,IAAI;AAAA,EACxB,MAAM,CAAC,iBAAiB,aAAa;AACvC;AACA,IAAI,gBAAgB;AAAA,EAClB,QAAQ,CAAC,KAAK,KAAK,KAAK,GAAG;AAAA,EAC3B,aAAa,CAAC,MAAM,MAAM,MAAM,IAAI;AAAA,EACpC,MAAM,CAAC,iBAAiB,gBAAgB,gBAAgB,cAAc;AACxE;AACA,IAAI,cAAc;AAAA,EAChB,QAAQ,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AAAA,EACnE,aAAa,CAAC,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAAA,EAChG,MAAM,CAAC,YAAY,aAAa,SAAS,SAAS,OAAO,SAAS,SAAS,YAAY,aAAa,WAAW,YAAY,UAAU;AACvI;AACA,IAAI,YAAY;AAAA,EACd,QAAQ,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,GAAG;AAAA,EAC1C,OAAO,CAAC,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,IAAI;AAAA,EAChD,aAAa,CAAC,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAAA,EAC7D,MAAM,CAAC,UAAU,WAAW,WAAW,YAAY,aAAa,UAAU,UAAU;AACtF;AACA,IAAI,kBAAkB;AAAA,EACpB,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,aAAa;AAAA,IACX,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AACF;AACA,IAAI,4BAA4B;AAAA,EAC9B,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,aAAa;AAAA,IACX,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,UAAU;AAAA,IACV,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,IACX,SAAS;AAAA,IACT,OAAO;AAAA,EACT;AACF;AACA,IAAI,gBAAgB,SAASC,eAAc,aAAa;AACtD,MAAI,SAAS,OAAO,WAAW;AAC/B,MAAI,SAAS,SAAS;AACtB,MAAI,SAAS,IAAI;AACf,YAAQ,QAAQ;AAAA,MACd,KAAK;AAAA,MACL,KAAK;AACH,eAAO,SAAS;AAAA,MAClB;AACE,eAAO,SAAS;AAAA,IACpB;AAAA,EACF;AACA,SAAO,SAAS;AAClB;AACA,IAAI,WAAW;AAAA,EACb;AAAA,EACA,KAAK,gBAAgB;AAAA,IACnB,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB,CAAC;AAAA,EACD,SAAS,gBAAgB;AAAA,IACvB,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,kBAAkB,SAAS,iBAAiB,SAAS;AACnD,aAAO,UAAU;AAAA,IACnB;AAAA,EACF,CAAC;AAAA,EACD,OAAO,gBAAgB;AAAA,IACrB,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB,CAAC;AAAA,EACD,KAAK,gBAAgB;AAAA,IACnB,QAAQ;AAAA,IACR,cAAc;AAAA,EAChB,CAAC;AAAA,EACD,WAAW,gBAAgB;AAAA,IACzB,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,kBAAkB;AAAA,IAClB,wBAAwB;AAAA,EAC1B,CAAC;AACH;AACA,IAAOC,oBAAQ;;;AC9Hf,IAAI,4BAA4B;AAChC,IAAI,4BAA4B;AAChC,IAAI,mBAAmB;AAAA,EACrB,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,MAAM;AACR;AACA,IAAI,mBAAmB;AAAA,EACrB,KAAK,CAAC,MAAM,IAAI;AAClB;AACA,IAAI,uBAAuB;AAAA,EACzB,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,MAAM;AACR;AACA,IAAI,uBAAuB;AAAA,EACzB,KAAK,CAAC,MAAM,MAAM,MAAM,IAAI;AAC9B;AACA,IAAI,qBAAqB;AAAA,EACvB,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,MAAM;AACR;AACA,IAAI,qBAAqB;AAAA,EACvB,QAAQ,CAAC,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,KAAK;AAAA,EAC3F,KAAK,CAAC,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,OAAO;AAClH;AACA,IAAI,mBAAmB;AAAA,EACrB,QAAQ;Line 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
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-PJEEZAML.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_react_dom();
|
||||
//# sourceMappingURL=react-dom.js.map
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-PJEEZAML.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/react-dom/client.js
|
||||
var require_client = __commonJS({
|
||||
"node_modules/react-dom/client.js"(exports) {
|
||||
var m = require_react_dom();
|
||||
if (false) {
|
||||
exports.createRoot = m.createRoot;
|
||||
exports.hydrateRoot = m.hydrateRoot;
|
||||
} else {
|
||||
i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
||||
exports.createRoot = function(c, o) {
|
||||
i.usingClientEntryPoint = true;
|
||||
try {
|
||||
return m.createRoot(c, o);
|
||||
} finally {
|
||||
i.usingClientEntryPoint = false;
|
||||
}
|
||||
};
|
||||
exports.hydrateRoot = function(c, h, o) {
|
||||
i.usingClientEntryPoint = true;
|
||||
try {
|
||||
return m.hydrateRoot(c, h, o);
|
||||
} finally {
|
||||
i.usingClientEntryPoint = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
var i;
|
||||
}
|
||||
});
|
||||
export default require_client();
|
||||
//# sourceMappingURL=react-dom_client.js.map
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../react-dom/client.js"],
|
||||
"sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n"],
|
||||
"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": []
|
||||
}
|
||||
-6454
File diff suppressed because it is too large.
Load diff
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../react-icons/fa/index.esm.js"],
|
||||
"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": ";;;;;;;AAEO,SAAS,QAAS,OAAO;AAC9B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,gpCAA+oC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC5vC;AACO,SAAS,iBAAkB,OAAO;AACvC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,0rBAAyrB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACtyB;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,uwBAAswB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACn3B;AACO,SAAS,2BAA4B,OAAO;AACjD,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,kyCAAiyC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC94C;AACO,SAAS,MAAO,OAAO;AAC5B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,8LAA6L,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC1S;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,6sCAA4sC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACzzC;AACO,SAAS,iBAAkB,OAAO;AACvC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,uUAAsU,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACnb;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,owBAAmwB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACh3B;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,8zBAA6zB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC16B;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,4pBAA2pB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACxwB;AACO,SAAS,YAAa,OAAO;AAClC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,6zGAA4zG,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACz6G;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,moBAAkoB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC/uB;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ghBAA+gB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC5nB;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,gTAA+S,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC5Z;AACO,SAAS,YAAa,OAAO;AAClC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,yxCAAwxC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACr4C;AACO,SAAS,gBAAiB,OAAO;AACtC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,k1DAAi1D,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC97D;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,kKAAiK,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC9Q;AACO,SAAS,cAAe,OAAO;AACpC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,yoBAAwoB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACrvB;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,6wBAA4wB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACz3B;AACO,SAAS,QAAS,OAAO;AAC9B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,onDAAmnD,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAChuD;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ylCAAwlC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACrsC;AACO,SAAS,QAAS,OAAO;AAC9B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,sbAAqb,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACliB;AACO,SAAS,aAAc,OAAO;AACnC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,oMAAmM,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAChT;AACO,SAAS,aAAc,OAAO;AACnC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,+qBAA8qB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC3xB;AACO,SAAS,YAAa,OAAO;AAClC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,iSAAgS,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC7Y;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ifAAgf,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC7lB;AACO,SAAS,eAAgB,OAAO;AACrC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,0MAAyM,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACtT;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,8eAA6e,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC1lB;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ojEAAmjE,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAChqE;AACO,SAAS,MAAO,OAAO;AAC5B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,yrEAAwrE,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACryE;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,uGAAsG,EAAC,CAAC,EALine truncated
|
||||
"names": []
|
||||
}
|
||||
-8106
File diff suppressed because it is too large.
Load diff
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../react-icons/fa6/index.esm.js"],
|
||||
"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": ";;;;;;;AAEO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ypBAAwpB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACrwB;AACO,SAAS,QAAS,OAAO;AAC9B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,gpCAA+oC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC5vC;AACO,SAAS,iBAAkB,OAAO;AACvC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,0rBAAyrB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACtyB;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,uwBAAswB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACn3B;AACO,SAAS,MAAO,OAAO;AAC5B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,8LAA6L,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC1S;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,6sCAA4sC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACzzC;AACO,SAAS,iBAAkB,OAAO;AACvC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,uUAAsU,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACnb;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,owBAAmwB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACh3B;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,6pBAA4pB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACzwB;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,4pBAA2pB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACxwB;AACO,SAAS,YAAa,OAAO;AAClC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,6zGAA4zG,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACz6G;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,moBAAkoB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC/uB;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ghBAA+gB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC5nB;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,gTAA+S,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC5Z;AACO,SAAS,YAAa,OAAO;AAClC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,yxCAAwxC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACr4C;AACO,SAAS,gBAAiB,OAAO;AACtC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,k1DAAi1D,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC97D;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,kKAAiK,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC9Q;AACO,SAAS,cAAe,OAAO;AACpC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,yoBAAwoB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACrvB;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,6wBAA4wB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACz3B;AACO,SAAS,QAAS,OAAO;AAC9B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,onDAAmnD,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAChuD;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ylCAAwlC,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACrsC;AACO,SAAS,QAAS,OAAO;AAC9B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,sbAAqb,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACliB;AACO,SAAS,aAAc,OAAO;AACnC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,oMAAmM,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAChT;AACO,SAAS,aAAc,OAAO;AACnC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,+qBAA8qB,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC3xB;AACO,SAAS,YAAa,OAAO;AAClC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,iSAAgS,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC7Y;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ifAAgf,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC7lB;AACO,SAAS,eAAgB,OAAO;AACrC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,0MAAyM,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACtT;AACO,SAAS,UAAW,OAAO;AAChC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,8eAA6e,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAC1lB;AACO,SAAS,SAAU,OAAO;AAC/B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,ojEAAmjE,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AAChqE;AACO,SAAS,MAAO,OAAO;AAC5B,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,yrEAAwrE,EAAC,CAAC,EAAC,CAAC,EAAE,KAAK;AACryE;AACO,SAAS,WAAY,OAAO;AACjC,SAAO,QAAQ,EAAC,OAAM,OAAM,QAAO,EAAC,WAAU,cAAa,GAAE,SAAQ,CAAC,EAAC,OAAM,QAAO,QAAO,EAAC,KAAI,uGAAsG,EAAC,CAAC,EAACLine truncated
|
||||
"names": []
|
||||
}
|
||||
-6055
File diff suppressed because it is too large.
Load diff
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../@remix-run/router/history.ts", "../../@remix-run/router/utils.ts", "../../@remix-run/router/router.ts", "../../react-router/lib/context.ts", "../../react-router/lib/hooks.tsx", "../../react-router/lib/deprecations.ts", "../../react-router/lib/components.tsx", "../../react-router/index.ts", "../../react-router-dom/dom.ts", "../../react-router-dom/index.tsx"],
|
||||
"sourcesContent": ["////////////////////////////////////////////////////////////////////////////////\n//#region Types and Constants\n////////////////////////////////////////////////////////////////////////////////\n\n/**\n * Actions represent the type of change to a location value.\n */\nexport enum Action {\n /**\n * A POP indicates a change to an arbitrary index in the history stack, such\n * as a back or forward navigation. It does not describe the direction of the\n * navigation, only that the current index changed.\n *\n * Note: This is the default action for newly created history objects.\n */\n Pop = \"POP\",\n\n /**\n * A PUSH indicates a new entry being added to the history stack, such as when\n * a link is clicked and a new page loads. When this happens, all subsequent\n * entries in the stack are lost.\n */\n Push = \"PUSH\",\n\n /**\n * A REPLACE indicates the entry at the current index in the history stack\n * being replaced by a new one.\n */\n Replace = \"REPLACE\",\n}\n\n/**\n * The pathname, search, and hash values of a URL.\n */\nexport interface Path {\n /**\n * A URL pathname, beginning with a /.\n */\n pathname: string;\n\n /**\n * A URL search string, beginning with a ?.\n */\n search: string;\n\n /**\n * A URL fragment identifier, beginning with a #.\n */\n hash: string;\n}\n\n// TODO: (v7) Change the Location generic default from `any` to `unknown` and\n// remove Remix `useLocation` wrapper.\n\n/**\n * An entry in a history stack. A location contains information about the\n * URL path, as well as possibly some arbitrary state and a key.\n */\nexport interface Location<State = any> extends Path {\n /**\n * A value of arbitrary data associated with this location.\n */\n state: State;\n\n /**\n * A unique string associated with this location. May be used to safely store\n * and retrieve data in some other storage API, like `localStorage`.\n *\n * Note: This value is always \"default\" on the initial location.\n */\n key: string;\n}\n\n/**\n * A change to the current location.\n */\nexport interface Update {\n /**\n * The action that triggered the change.\n */\n action: Action;\n\n /**\n * The new location.\n */\n location: Location;\n\n /**\n * The delta between this location and the former location in the history stack\n */\n delta: number | null;\n}\n\n/**\n * A function that receives notifications about location changes.\n */\nexport interface Listener {\n (update: Update): void;\n}\n\n/**\n * Describes a location that is the destination of some navigation, either via\n * `history.push` or `history.replace`. This may be either a URL or the pieces\n * of a URL path.\n */\nexport type To = string | Partial<Path>;\n\n/**\n * A history is an interface to the navigation stack. The history serves as the\n * source of truth for the current location, as well as provides a set of\n * methods that may be used to change it.\n *\n * It is similar to the DOM's `window.history` object, but with a smaller, more\n * focused API.\n */\nexport interface History {\n /**\n * The last action that modified the current location. This will always be\n * Action.Pop when a history instance is first created. This value is mutable.\n */\n readonly action: Action;\n\n /**\n * The current location. This value is mutable.\n */\n readonly location: Location;\n\n /**\n * Returns a valid href for the given `to` value that may be used as\n * the value of an <a href> attribute.\n *\n * @param to - The destination URL\n */\n createHref(to: To): string;\n\n /**\n * Returns a URL for the given `to` value\n *\n * @param to - The destination URL\n */\n createURL(to: To): URL;\n\n /**\n * Encode a location the same way window.history would do (no-op for memory\n * history) so we ensure our PUSH/REPLACE navigations for data routers\n * behave the same as POP\n *\n * @param to Unencoded path\n */\n encodeLocation(to: To): Path;\n\n /**\n * Pushes a new location onto the history stack, increasing its length by one.\n * If there were any entries in the stack after the current one, they are\n * lost.\n *\n * @param to - The new URL\n * @param state - Data to associate with the new location\n */\n push(to: To, state?: any): void;\n\n /**\n * Replaces the current location in the history stack with a new one. The\n * location that was replaced will no longer be available.\n *\n * @param to - The new URL\n * @param state - Data to associate with the new location\n */\n replace(to: To, state?: any): void;\n\n /**\n * Navigates `n` entries backward/forward in the history stack relative to the\n * current index. For example, a \"back\" navigation would use go(-1).\n *\n * @param delta - The delta in the stack index\n */\n go(delta: number): void;\n\n /**\n * Sets up a listener that will be called whenever the current location\n * changes.\n *\n * @Line truncated
|
||||
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAOYA;CAAZ,SAAYA,SAAM;AAQhBA,EAAAA,QAAA,KAAA,IAAA;AAOAA,EAAAA,QAAA,MAAA,IAAA;AAMAA,EAAAA,QAAA,SAAA,IAAA;AACF,GAtBYA,WAAAA,SAsBX,CAAA,EAAA;AAqKD,IAAMC,oBAAoB;AAmCV,SAAAC,oBACdC,SAAkC;AAAA,MAAlCA,YAAA,QAAA;AAAAA,cAAgC,CAAA;EAAE;AAElC,MAAI;IAAEC,iBAAiB,CAAC,GAAG;IAAGC;IAAcC,WAAW;EAAO,IAAGH;AACjE,MAAII;AACJA,YAAUH,eAAeI,IAAI,CAACC,OAAOC,WACnCC,qBACEF,OACA,OAAOA,UAAU,WAAW,OAAOA,MAAMG,OACzCF,WAAU,IAAI,YAAYG,MAAS,CACpC;AAEH,MAAIH,QAAQI,WACVT,gBAAgB,OAAOE,QAAQQ,SAAS,IAAIV,YAAY;AAE1D,MAAIW,SAAShB,OAAOiB;AACpB,MAAIC,WAA4B;AAEhC,WAASJ,WAAWK,GAAS;AAC3B,WAAOC,KAAKC,IAAID,KAAKE,IAAIH,GAAG,CAAC,GAAGZ,QAAQQ,SAAS,CAAC;EACpD;AACA,WAASQ,qBAAkB;AACzB,WAAOhB,QAAQG,KAAK;EACtB;AACA,WAASC,qBACPa,IACAZ,OACAa,KAAY;AAAA,QADZb,UAAa,QAAA;AAAbA,cAAa;IAAI;AAGjB,QAAIc,WAAWC,eACbpB,UAAUgB,mBAAkB,EAAGK,WAAW,KAC1CJ,IACAZ,OACAa,GAAG;AAELI,YACEH,SAASE,SAASE,OAAO,CAAC,MAAM,KAAG,6DACwBC,KAAKC,UAC9DR,EAAE,CACD;AAEL,WAAOE;EACT;AAEA,WAASO,WAAWT,IAAM;AACxB,WAAO,OAAOA,OAAO,WAAWA,KAAKU,WAAWV,EAAE;EACpD;AAEA,MAAIW,UAAyB;IAC3B,IAAIzB,QAAK;AACP,aAAOA;;IAET,IAAIM,SAAM;AACR,aAAOA;;IAET,IAAIU,WAAQ;AACV,aAAOH,mBAAkB;;IAE3BU;IACAG,UAAUZ,IAAE;AACV,aAAO,IAAIa,IAAIJ,WAAWT,EAAE,GAAG,kBAAkB;;IAEnDc,eAAed,IAAM;AACnB,UAAIe,OAAO,OAAOf,OAAO,WAAWgB,UAAUhB,EAAE,IAAIA;AACpD,aAAO;QACLI,UAAUW,KAAKX,YAAY;QAC3Ba,QAAQF,KAAKE,UAAU;QACvBC,MAAMH,KAAKG,QAAQ;;;IAGvBC,KAAKnB,IAAIZ,OAAK;AACZI,eAAShB,OAAO4C;AAChB,UAAIC,eAAelC,qBAAqBa,IAAIZ,KAAK;AACjDF,eAAS;AACTH,cAAQuC,OAAOpC,OAAOH,QAAQQ,QAAQ8B,YAAY;AAClD,UAAIvC,YAAYY,UAAU;AACxBA,iBAAS;UAAEF;UAAQU,UAAUmB;UAAcE,OAAO;QAAC,CAAE;MACtD;;IAEHC,QAAQxB,IAAIZ,OAAK;AACfI,eAAShB,OAAOiD;AAChB,UAAIJ,eAAelC,qBAAqBa,IAAIZ,KAAK;AACjDL,cAAQG,KAAK,IAAImC;AACjB,UAAIvC,YAAYY,UAAU;AACxBA,iBAAS;UAAEF;UAAQU,UAAUmB;UAAcE,OAAO;QAAC,CAAE;MACtD;;IAEHG,GAAGH,OAAK;AACN/B,eAAShB,OAAOiB;AAChB,UAAIkC,YAAYrC,WAAWJ,QAAQqC,KAAK;AACxC,UAAIF,eAAetC,QAAQ4C,SAAS;AACpCzC,cAAQyC;AACR,UAAIjC,UAAU;AACZA,iBAAS;UAAEF;UAAQU,UAAUmB;UAAcE;QAAO,CAAA;MACnD;;IAEHK,OAAOC,IAAY;AACjBnC,iBAAWmC;AACX,aAAO,MAAK;AACVnC,mBAAW;;IAEf;;AAGF,SAAOiB;AACT;AAyBgB,SAAAmB,qBACdnD,SAAmC;AAAA,MAAnCA,YAAA,QAAA;AAAAA,cAAiC,CAAA;EAAE;AAEnC,WAASoD,sBACPC,SACAC,eAAgC;AAEhC,QAAI;MAAE7B;MAAUa;MAAQC;QAASc,QAAO9B;AACxC,WAAOC;MACL;MACA;QAAEC;QAAUa;QAAQC;;;MAEnBe,cAAc7C,SAAS6C,cAAc7C,MAAM8C,OAAQ;MACnDD,cAAc7C,SAAS6C,cAAc7C,MAAMa,OAAQ;IAAS;EAEjE;AAEA,WAASkC,kBAAkBH,SAAgBhC,IAAM;AAC/C,WAAO,OAAOA,OAAO,WAAWA,KAAKU,WAAWV,EAAE;EACpD;AAEA,SAAOoC,mBACLL,uBACAI,mBACA,MACAxD,OAAO;AAEX;AA8BgB,SAAA0D,kBACd1D,SAAgC;AAAA,MAAhCA,YAAA,QAAA;AAAAA,cAA8B,CAAA;EAAE;AAEhC,WAAS2D,mBACPN,SACAC,eAAgC;AAEhC,QAAI;MACF7B,WAAW;MACXa,SAAS;MACTC,OAAO;IAAE,IACPF,UAAUgB,QAAO9B,SAASgB,KAAKqB,OAAO,CAAC,CAAC;AAQ5C,QAAI,CAACnC,SAASoC,WAAW,GAAG,KAAK,CAACpC,SAASoC,WAAW,GAAG,GAAG;AAC1DpC,iBAAW,MAAMA;IAClB;AAED,WAAOD;MACL;MACA;QAAEC;QAAUa;QAAQC;;;MAEnBe,cAAc7C,SAAS6C,cAAc7C,MAAM8C,OAAQ;MACnDD,cAAc7C,SAAS6C,cAAc7C,MAAMa,OAAQ;IAAS;EAEjE;AAEA,WAASwC,eAAeT,SAAgBhC,IAAM;AAC5C,QAAI0C,OAAOV,QAAOW,SAASC,cAAc,MAAM;AAC/C,QAAIC,OAAO;AAEX,QAAIH,QAAQA,KAAKI,aAAa,MAAM,GAAG;AACrC,UAAIC,MAAMf,QAAO9B,SAAS2C;AAC1B,UAAIG,YAAYD,IAAIE,QAAQ,GAAG;AAC/BJ,aAAOG,cAAc,KAAKD,MAAMA,IAAIG,MAAM,GAAGF,SAAS;IACvD;AAED,WAAOH,OAAO,OAAO,OAAO7C,OAAO,WAAWA,KAAKU,WAAWV,EAAE;EAClE;AAEA,WAASmD,qBAAqBjD,UAAoBF,IAAM;AACtDK,YACEH,SAASE,SAASE,OAAO,CAAC,MAAM,KAAG,+DAC0BC,KAAKC,UAChER,EAAE,IACH,GAAG;EAER;AAEA,SAAOoC,mBACLE,oBACAG,gBACAU,sBACAxE,OAAO;AAEX;AAegB,SAAAyE,UAAUC,OAAYC,SAAgB;AACpD,MAAID,UAAU,SAASA,UAAU,QAAQ,OAAOA,UAAU,aAAa;AACrE,UAAM,IAAIE,MAAMD,OAAO;EACxB;AACH;AAEgB,SAAAjD,QAAQmD,MAAWF,SAAe;AAChD,MAAI,CAACE,MAAM;AAET,QAAI,OAAOC,YAAY,YAAaA,SAAQC,KAAKJ,OAAO;AAExD,QAAI;AAMF,YAAM,IAAIC,MAAMD,OAAO;IAExB,SAAQK,GAAG;IAAA;EACb;AACH;AAEA,SAASC,YAAS;AAChB,SAAOhE,KAAKiE,OAAM,EAAGC,SAAS,EAAE,EAAEvB,OAAO,GAAG,CAAC;AAC/C;AAKA,SAASwB,gBAAgB7D,UAAoBhB,OAAa;AACxD,SAAO;IACLgD,KAAKhC,SAASd;IACda,KAAKC,SAASD;IACd+D,KAAK9E;;AAET;AAKM,SAAUiB,eACd8D,SACAjE,IACAZ,OACAa,KAAY;AAAA,MADZb,UAAA,QAAA;AAAAA,YAAa;EAAI;AAGjB,MAAIc,WAAQgE,SAAA;IACV9D,UAAU,OAAO6D,YAAY,WAAWA,UAAUA,QAAQ7D;IAC1Da,QAAQ;IACRC,MAAM;KACF,OAAOlB,OAAO,WAAWgB,UAAUhB,EAAE,IAAIA,IAAE;IAC/CZ;;;;;IAKAa,KAAMD,MAAOA,GAAgBC,OAAQA,OAAO2D,UAAS;GACtD;AACD,SAAO1D;AACT;AAKgB,SAAAQ,WAAUyD,MAIV;AAAA,MAJW;IACzB/D,WAAW;IACXa,SAAS;IACTC,OAAO;EACO,IAAAiD;AACd,MAAIlD,UAAUA,WAAW,IACvBb,aAAYa,OAAOX,OAAO,CAAC,MAAM,MAAMW,SAAS,MAAMA;AACxD,MAAIC,QAAQA,SAAS,IACnBd,aAAYc,KAAKZ,OAAO,CAAC,MAAM,MAAMY,OAAO,MAAMA;AACpD,SAAOd;AACT;AAKM,SAAUY,UAAUD,MAAY;AACpC,MAAIqD,aAA4B,CAAA;AAEhC,MAAIrD,MAAM;AACR,QAAIiC,YAAYjC,KAAKkC,QAAQ,GAAG;AAChC,QAAID,aAAa,GAAG;AAClBoB,iBAAWlD,OAAOH,KAAKwB,OAAOS,SAAS;AACvCjC,aAAOA,KAAKwB,OAAO,GAAGS,SAAS;IAChC;AAED,QAAIqB,cAActD,KAAKkC,QAAQ,GAAG;AAClC,QAAIoB,eAAe,GAAG;AACpBD,iBAAWnD,SAASF,KAAKwB,OAAO8B,WAAW;AAC3CtD,aAAOA,KAAKwB,OAAO,GAAG8B,WAAW;IAClC;AAED,QAAItD,MAAM;AACRqD,iBAAWhE,WAAWW;IACvB;EACF;AAED,SAAOqD;AACT;AASA,SAAShC,mBACPkC,aACA7D,YACA8D,kBACA5F,SAA+B;AAAA,MAA/BA,YAAA,QAAA;AAAAA,cAA6B,CAAA;EAAE;AAE/B,MAAI;IAAEqD,QAAAA,UAASW,SAAS6B;IAAc1F,WAAW;EAAO,IAAGH;AAC3D,MAAIsD,gBAAgBD,QAAOrB;AAC3B,MAAInB,SAAShB,Line truncated
|
||||
"names": ["Action", "PopStateEventType", "createMemoryHistory", "options", "initialEntries", "initialIndex", "v5Compat", "entries", "map", "entry", "index", "createMemoryLocation", "state", "undefined", "clampIndex", "length", "action", "Pop", "listener", "n", "Math", "min", "max", "getCurrentLocation", "to", "key", "location", "createLocation", "pathname", "warning", "charAt", "JSON", "stringify", "createHref", "createPath", "history", "createURL", "URL", "encodeLocation", "path", "parsePath", "search", "hash", "push", "Push", "nextLocation", "splice", "delta", "replace", "Replace", "go", "nextIndex", "listen", "fn", "createBrowserHistory", "createBrowserLocation", "window", "globalHistory", "usr", "createBrowserHref", "getUrlBasedHistory", "createHashHistory", "createHashLocation", "substr", "startsWith", "createHashHref", "base", "document", "querySelector", "href", "getAttribute", "url", "hashIndex", "indexOf", "slice", "validateHashLocation", "invariant", "value", "message", "Error", "cond", "console", "warn", "e", "createKey", "random", "toString", "getHistoryState", "idx", "current", "_extends", "_ref", "parsedPath", "searchIndex", "getLocation", "validateLocation", "defaultView", "getIndex", "replaceState", "handlePop", "historyState", "pushState", "error", "DOMException", "name", "assign", "origin", "addEventListener", "removeEventListener", "ResultType", "immutableRouteKeys", "Set", "isIndexRoute", "route", "convertRoutesToDataRoutes", "routes", "mapRouteProperties", "parentPath", "manifest", "treePath", "String", "id", "join", "children", "indexRoute", "pathOrLayoutRoute", "matchRoutes", "locationArg", "basename", "matchRoutesImpl", "allowPartial", "stripBasename", "branches", "flattenRoutes", "rankRouteBranches", "matches", "i", "decoded", "decodePath", "matchRouteBranch", "convertRouteMatchToUiMatch", "match", "loaderData", "params", "data", "handle", "parentsMeta", "flattenRoute", "relativePath", "meta", "caseSensitive", "childrenIndex", "joinPaths", "routesMeta", "concat", "score", "computeScore", "forEach", "_route$path", "includes", "exploded", "explodeOptionalSegments", "segments", "split", "first", "rest", "isOptional", "endsWith", "required", "restExploded", "result", "subpath", "sort", "a", "b", "compareIndexes", "paramRe", "dynamicSegmentValue", "indexRouteValue", "emptySegmentValue", "staticSegmentValue", "splatPenalty", "isSplat", "s", "initialScore", "some", "filter", "reduce", "segment", "test", "siblings", "every", "branch", "matchedParams", "matchedPathname", "end", "remainingPathname", "matchPath", "Object", "pathnameBase", "normalizePathname", "generatePath", "originalPath", "prefix", "p", "array", "isLastSegment", "star", "keyMatch", "optional", "param", "pattern", "matcher", "compiledParams", "compilePath", "captureGroups", "memo", "paramName", "splatValue", "regexpSource", "_", "RegExp", "v", "decodeURIComponent", "toLowerCase", "startIndex", "nextChar", "ABSOLUTE_URL_REGEX", "isAbsoluteUrl", "resolvePath", "fromPathname", "toPathname", "oldPathname", "resolvePathname", "substring", "normalizeSearch", "normalizeHash", "relativeSegments", "pop", "getInvalidPathError", "char", "field", "dest", "getPathContributingMatches", "getResolveToMatches", "v7_relativeSplatPath", "pathMatches", "resolveTo", "toArg", "routePathnames", "locationPathname", "isPathRelative", "isEmptyPath", "from", "routePathnameIndex", "toSegments", "shift", "hasExplicitTrailingSlash", "hasCurrentTrailingSlash", "joinPaths", "paths", "join", "replace", "normalizePathname", "pathname", "normalizeSearch", "search", "startsWith", "normalizeHash", "hash", "json", "data", "init", "responseInit", "status", "headers", "Headers", "has", "set", "Response", "JSON", "stringify", "_extends", "AbortedDeferredError", "Error", "DeferredData", "constructor", "data", "responseInit", "pendingKeysSet", "Set", "subscribers", "deferredKeys", "invariant", "Array", "isArray", "reject", "abortPromise", "Promise", "_", "r", "controller", "AbortController", "onAbort", "unlistenAbortSignal", "signal", "removeEventListener", "addEventListener", "Object", "entries", "reduce", "acc", "_ref2", "key", "value", "assign", "trackPromise", "done", "init", "push", "add", "promise", "race", "then", "onSettle", "undefined", "error", "catch", "defineProperty", "get", "aborted", "delete", "undefinedError", "emit", "settledKey", "forEach", "subscriber", "subscribe", "fn", "cancel", "abort", "v", "k", "resolveData", "resolve", "size", "unwrappedData", "_ref3", "unwrapTrackedPromise", "pendingKeys", "from", "isTrackedPromise", "_tracked", "_error", "_data", "defer", "status", "redirect", "url", "headers", "Headers", "set", "Response", "_extends", "redirectDocument", "response", "replace", "ErrorResponseImpl", "statusText", "internal", "toString", "isRouteErrorResponse", "validMutationMethodsArr", "validMutationMethods", "validRequestMethodsArr", "validRequestMethods", "redirectStatusCodes", "redirectPreserveMethodStatusCodes", "IDLE_NAVIGATION", "sLine truncated
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-DRWLMN53.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_react();
|
||||
//# sourceMappingURL=react.js.map
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
-913
@@ -1,913 +0,0 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-DRWLMN53.js";
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/react/cjs/react-jsx-dev-runtime.development.js
|
||||
var require_react_jsx_dev_runtime_development = __commonJS({
|
||||
"node_modules/react/cjs/react-jsx-dev-runtime.development.js"(exports) {
|
||||
"use strict";
|
||||
if (true) {
|
||||
(function() {
|
||||
"use strict";
|
||||
var React = require_react();
|
||||
var REACT_ELEMENT_TYPE = Symbol.for("react.element");
|
||||
var REACT_PORTAL_TYPE = Symbol.for("react.portal");
|
||||
var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
|
||||
var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode");
|
||||
var REACT_PROFILER_TYPE = Symbol.for("react.profiler");
|
||||
var REACT_PROVIDER_TYPE = Symbol.for("react.provider");
|
||||
var REACT_CONTEXT_TYPE = Symbol.for("react.context");
|
||||
var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
|
||||
var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense");
|
||||
var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list");
|
||||
var REACT_MEMO_TYPE = Symbol.for("react.memo");
|
||||
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
|
||||
var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen");
|
||||
var MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
|
||||
var FAUX_ITERATOR_SYMBOL = "@@iterator";
|
||||
function getIteratorFn(maybeIterable) {
|
||||
if (maybeIterable === null || typeof maybeIterable !== "object") {
|
||||
return null;
|
||||
}
|
||||
var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];
|
||||
if (typeof maybeIterator === "function") {
|
||||
return maybeIterator;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
||||
function error(format) {
|
||||
{
|
||||
{
|
||||
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||||
args[_key2 - 1] = arguments[_key2];
|
||||
}
|
||||
printWarning("error", format, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
function printWarning(level, format, args) {
|
||||
{
|
||||
var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
var stack = ReactDebugCurrentFrame2.getStackAddendum();
|
||||
if (stack !== "") {
|
||||
format += "%s";
|
||||
args = args.concat([stack]);
|
||||
}
|
||||
var argsWithFormat = args.map(function(item) {
|
||||
return String(item);
|
||||
});
|
||||
argsWithFormat.unshift("Warning: " + format);
|
||||
Function.prototype.apply.call(console[level], console, argsWithFormat);
|
||||
}
|
||||
}
|
||||
var enableScopeAPI = false;
|
||||
var enableCacheElement = false;
|
||||
var enableTransitionTracing = false;
|
||||
var enableLegacyHidden = false;
|
||||
var enableDebugTracing = false;
|
||||
var REACT_MODULE_REFERENCE;
|
||||
{
|
||||
REACT_MODULE_REFERENCE = Symbol.for("react.module.reference");
|
||||
}
|
||||
function isValidElementType(type) {
|
||||
if (typeof type === "string" || typeof type === "function") {
|
||||
return true;
|
||||
}
|
||||
if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing) {
|
||||
return true;
|
||||
}
|
||||
if (typeof type === "object" && type !== null) {
|
||||
if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object
|
||||
// types supported by any Flight configuration anywhere since
|
||||
// we don't know which Flight build this will end up being used
|
||||
// with.
|
||||
type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== void 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function getWrappedName(outerType, innerType, wrapperName) {
|
||||
var displayName = outerType.displayName;
|
||||
if (displayName) {
|
||||
return displayName;
|
||||
}
|
||||
var functionName = innerType.displayName || innerType.name || "";
|
||||
return functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName;
|
||||
}
|
||||
function getContextName(type) {
|
||||
return type.displayName || "Context";
|
||||
}
|
||||
function getComponentNameFromType(type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
{
|
||||
if (typeof type.tag === "number") {
|
||||
error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.");
|
||||
}
|
||||
}
|
||||
if (typeof type === "function") {
|
||||
return type.displayName || type.name || null;
|
||||
}
|
||||
if (typeof type === "string") {
|
||||
return type;
|
||||
}
|
||||
switch (type) {
|
||||
case REACT_FRAGMENT_TYPE:
|
||||
return "Fragment";
|
||||
case REACT_PORTAL_TYPE:
|
||||
return "Portal";
|
||||
case REACT_PROFILER_TYPE:
|
||||
return "Profiler";
|
||||
case REACT_STRICT_MODE_TYPE:
|
||||
return "StrictMode";
|
||||
case REACT_SUSPENSE_TYPE:
|
||||
return "Suspense";
|
||||
case REACT_SUSPENSE_LIST_TYPE:
|
||||
return "SuspenseList";
|
||||
}
|
||||
if (typeof type === "object") {
|
||||
switch (type.$$typeof) {
|
||||
case REACT_CONTEXT_TYPE:
|
||||
var context = type;
|
||||
return getContextName(context) + ".Consumer";
|
||||
case REACT_PROVIDER_TYPE:
|
||||
var provider = type;
|
||||
return getContextName(provider._context) + ".Provider";
|
||||
case REACT_FORWARD_REF_TYPE:
|
||||
return getWrappedName(type, type.render, "ForwardRef");
|
||||
case REACT_MEMO_TYPE:
|
||||
var outerName = type.displayName || null;
|
||||
if (outerName !== null) {
|
||||
return outerName;
|
||||
}
|
||||
return getComponentNameFromType(type.type) || "Memo";
|
||||
case REACT_LAZY_TYPE: {
|
||||
var lazyComponent = type;
|
||||
var payload = lazyComponent._payload;
|
||||
var init = lazyComponent._init;
|
||||
try {
|
||||
return getComponentNameFromType(init(payload));
|
||||
} catch (x) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
var assign = Object.assign;
|
||||
var disabledDepth = 0;
|
||||
var prevLog;
|
||||
var prevInfo;
|
||||
var prevWarn;
|
||||
var prevError;
|
||||
var prevGroup;
|
||||
var prevGroupCollapsed;
|
||||
var prevGroupEnd;
|
||||
function disabledLog() {
|
||||
}
|
||||
disabledLog.__reactDisabledLog = true;
|
||||
function disableLogs() {
|
||||
{
|
||||
if (disabledDepth === 0) {
|
||||
prevLog = console.log;
|
||||
prevInfo = console.info;
|
||||
prevWarn = console.warn;
|
||||
prevError = console.error;
|
||||
prevGroup = console.group;
|
||||
prevGroupCollapsed = console.groupCollapsed;
|
||||
prevGroupEnd = console.groupEnd;
|
||||
var props = {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: disabledLog,
|
||||
writable: true
|
||||
};
|
||||
Object.defineProperties(console, {
|
||||
info: props,
|
||||
log: props,
|
||||
warn: props,
|
||||
error: props,
|
||||
group: props,
|
||||
groupCollapsed: props,
|
||||
groupEnd: props
|
||||
});
|
||||
}
|
||||
disabledDepth++;
|
||||
}
|
||||
}
|
||||
function reenableLogs() {
|
||||
{
|
||||
disabledDepth--;
|
||||
if (disabledDepth === 0) {
|
||||
var props = {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
writable: true
|
||||
};
|
||||
Object.defineProperties(console, {
|
||||
log: assign({}, props, {
|
||||
value: prevLog
|
||||
}),
|
||||
info: assign({}, props, {
|
||||
value: prevInfo
|
||||
}),
|
||||
warn: assign({}, props, {
|
||||
value: prevWarn
|
||||
}),
|
||||
error: assign({}, props, {
|
||||
value: prevError
|
||||
}),
|
||||
group: assign({}, props, {
|
||||
value: prevGroup
|
||||
}),
|
||||
groupCollapsed: assign({}, props, {
|
||||
value: prevGroupCollapsed
|
||||
}),
|
||||
groupEnd: assign({}, props, {
|
||||
value: prevGroupEnd
|
||||
})
|
||||
});
|
||||
}
|
||||
if (disabledDepth < 0) {
|
||||
error("disabledDepth fell below zero. This is a bug in React. Please file an issue.");
|
||||
}
|
||||
}
|
||||
}
|
||||
var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
|
||||
var prefix;
|
||||
function describeBuiltInComponentFrame(name, source, ownerFn) {
|
||||
{
|
||||
if (prefix === void 0) {
|
||||
try {
|
||||
throw Error();
|
||||
} catch (x) {
|
||||
var match = x.stack.trim().match(/\n( *(at )?)/);
|
||||
prefix = match && match[1] || "";
|
||||
}
|
||||
}
|
||||
return "\n" + prefix + name;
|
||||
}
|
||||
}
|
||||
var reentry = false;
|
||||
var componentFrameCache;
|
||||
{
|
||||
var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map;
|
||||
componentFrameCache = new PossiblyWeakMap();
|
||||
}
|
||||
function describeNativeComponentFrame(fn, construct) {
|
||||
if (!fn || reentry) {
|
||||
return "";
|
||||
}
|
||||
{
|
||||
var frame = componentFrameCache.get(fn);
|
||||
if (frame !== void 0) {
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
var control;
|
||||
reentry = true;
|
||||
var previousPrepareStackTrace = Error.prepareStackTrace;
|
||||
Error.prepareStackTrace = void 0;
|
||||
var previousDispatcher;
|
||||
{
|
||||
previousDispatcher = ReactCurrentDispatcher.current;
|
||||
ReactCurrentDispatcher.current = null;
|
||||
disableLogs();
|
||||
}
|
||||
try {
|
||||
if (construct) {
|
||||
var Fake = function() {
|
||||
throw Error();
|
||||
};
|
||||
Object.defineProperty(Fake.prototype, "props", {
|
||||
set: function() {
|
||||
throw Error();
|
||||
}
|
||||
});
|
||||
if (typeof Reflect === "object" && Reflect.construct) {
|
||||
try {
|
||||
Reflect.construct(Fake, []);
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
Reflect.construct(fn, [], Fake);
|
||||
} else {
|
||||
try {
|
||||
Fake.call();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
fn.call(Fake.prototype);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
throw Error();
|
||||
} catch (x) {
|
||||
control = x;
|
||||
}
|
||||
fn();
|
||||
}
|
||||
} catch (sample) {
|
||||
if (sample && control && typeof sample.stack === "string") {
|
||||
var sampleLines = sample.stack.split("\n");
|
||||
var controlLines = control.stack.split("\n");
|
||||
var s = sampleLines.length - 1;
|
||||
var c = controlLines.length - 1;
|
||||
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
|
||||
c--;
|
||||
}
|
||||
for (; s >= 1 && c >= 0; s--, c--) {
|
||||
if (sampleLines[s] !== controlLines[c]) {
|
||||
if (s !== 1 || c !== 1) {
|
||||
do {
|
||||
s--;
|
||||
c--;
|
||||
if (c < 0 || sampleLines[s] !== controlLines[c]) {
|
||||
var _frame = "\n" + sampleLines[s].replace(" at new ", " at ");
|
||||
if (fn.displayName && _frame.includes("<anonymous>")) {
|
||||
_frame = _frame.replace("<anonymous>", fn.displayName);
|
||||
}
|
||||
{
|
||||
if (typeof fn === "function") {
|
||||
componentFrameCache.set(fn, _frame);
|
||||
}
|
||||
}
|
||||
return _frame;
|
||||
}
|
||||
} while (s >= 1 && c >= 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reentry = false;
|
||||
{
|
||||
ReactCurrentDispatcher.current = previousDispatcher;
|
||||
reenableLogs();
|
||||
}
|
||||
Error.prepareStackTrace = previousPrepareStackTrace;
|
||||
}
|
||||
var name = fn ? fn.displayName || fn.name : "";
|
||||
var syntheticFrame = name ? describeBuiltInComponentFrame(name) : "";
|
||||
{
|
||||
if (typeof fn === "function") {
|
||||
componentFrameCache.set(fn, syntheticFrame);
|
||||
}
|
||||
}
|
||||
return syntheticFrame;
|
||||
}
|
||||
function describeFunctionComponentFrame(fn, source, ownerFn) {
|
||||
{
|
||||
return describeNativeComponentFrame(fn, false);
|
||||
}
|
||||
}
|
||||
function shouldConstruct(Component) {
|
||||
var prototype = Component.prototype;
|
||||
return !!(prototype && prototype.isReactComponent);
|
||||
}
|
||||
function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) {
|
||||
if (type == null) {
|
||||
return "";
|
||||
}
|
||||
if (typeof type === "function") {
|
||||
{
|
||||
return describeNativeComponentFrame(type, shouldConstruct(type));
|
||||
}
|
||||
}
|
||||
if (typeof type === "string") {
|
||||
return describeBuiltInComponentFrame(type);
|
||||
}
|
||||
switch (type) {
|
||||
case REACT_SUSPENSE_TYPE:
|
||||
return describeBuiltInComponentFrame("Suspense");
|
||||
case REACT_SUSPENSE_LIST_TYPE:
|
||||
return describeBuiltInComponentFrame("SuspenseList");
|
||||
}
|
||||
if (typeof type === "object") {
|
||||
switch (type.$$typeof) {
|
||||
case REACT_FORWARD_REF_TYPE:
|
||||
return describeFunctionComponentFrame(type.render);
|
||||
case REACT_MEMO_TYPE:
|
||||
return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);
|
||||
case REACT_LAZY_TYPE: {
|
||||
var lazyComponent = type;
|
||||
var payload = lazyComponent._payload;
|
||||
var init = lazyComponent._init;
|
||||
try {
|
||||
return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn);
|
||||
} catch (x) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var loggedTypeFailures = {};
|
||||
var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
function setCurrentlyValidatingElement(element) {
|
||||
{
|
||||
if (element) {
|
||||
var owner = element._owner;
|
||||
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
|
||||
ReactDebugCurrentFrame.setExtraStackFrame(stack);
|
||||
} else {
|
||||
ReactDebugCurrentFrame.setExtraStackFrame(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
function checkPropTypes(typeSpecs, values, location, componentName, element) {
|
||||
{
|
||||
var has = Function.call.bind(hasOwnProperty);
|
||||
for (var typeSpecName in typeSpecs) {
|
||||
if (has(typeSpecs, typeSpecName)) {
|
||||
var error$1 = void 0;
|
||||
try {
|
||||
if (typeof typeSpecs[typeSpecName] !== "function") {
|
||||
var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
|
||||
err.name = "Invariant Violation";
|
||||
throw err;
|
||||
}
|
||||
error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
|
||||
} catch (ex) {
|
||||
error$1 = ex;
|
||||
}
|
||||
if (error$1 && !(error$1 instanceof Error)) {
|
||||
setCurrentlyValidatingElement(element);
|
||||
error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
|
||||
setCurrentlyValidatingElement(null);
|
||||
}
|
||||
if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
|
||||
loggedTypeFailures[error$1.message] = true;
|
||||
setCurrentlyValidatingElement(element);
|
||||
error("Failed %s type: %s", location, error$1.message);
|
||||
setCurrentlyValidatingElement(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var isArrayImpl = Array.isArray;
|
||||
function isArray(a) {
|
||||
return isArrayImpl(a);
|
||||
}
|
||||
function typeName(value) {
|
||||
{
|
||||
var hasToStringTag = typeof Symbol === "function" && Symbol.toStringTag;
|
||||
var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
|
||||
return type;
|
||||
}
|
||||
}
|
||||
function willCoercionThrow(value) {
|
||||
{
|
||||
try {
|
||||
testStringCoercion(value);
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
function testStringCoercion(value) {
|
||||
return "" + value;
|
||||
}
|
||||
function checkKeyStringCoercion(value) {
|
||||
{
|
||||
if (willCoercionThrow(value)) {
|
||||
error("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value));
|
||||
return testStringCoercion(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
var ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
|
||||
var RESERVED_PROPS = {
|
||||
key: true,
|
||||
ref: true,
|
||||
__self: true,
|
||||
__source: true
|
||||
};
|
||||
var specialPropKeyWarningShown;
|
||||
var specialPropRefWarningShown;
|
||||
var didWarnAboutStringRefs;
|
||||
{
|
||||
didWarnAboutStringRefs = {};
|
||||
}
|
||||
function hasValidRef(config) {
|
||||
{
|
||||
if (hasOwnProperty.call(config, "ref")) {
|
||||
var getter = Object.getOwnPropertyDescriptor(config, "ref").get;
|
||||
if (getter && getter.isReactWarning) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return config.ref !== void 0;
|
||||
}
|
||||
function hasValidKey(config) {
|
||||
{
|
||||
if (hasOwnProperty.call(config, "key")) {
|
||||
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
|
||||
if (getter && getter.isReactWarning) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return config.key !== void 0;
|
||||
}
|
||||
function warnIfStringRefCannotBeAutoConverted(config, self) {
|
||||
{
|
||||
if (typeof config.ref === "string" && ReactCurrentOwner.current && self && ReactCurrentOwner.current.stateNode !== self) {
|
||||
var componentName = getComponentNameFromType(ReactCurrentOwner.current.type);
|
||||
if (!didWarnAboutStringRefs[componentName]) {
|
||||
error('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref', getComponentNameFromType(ReactCurrentOwner.current.type), config.ref);
|
||||
didWarnAboutStringRefs[componentName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function defineKeyPropWarningGetter(props, displayName) {
|
||||
{
|
||||
var warnAboutAccessingKey = function() {
|
||||
if (!specialPropKeyWarningShown) {
|
||||
specialPropKeyWarningShown = true;
|
||||
error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
|
||||
}
|
||||
};
|
||||
warnAboutAccessingKey.isReactWarning = true;
|
||||
Object.defineProperty(props, "key", {
|
||||
get: warnAboutAccessingKey,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
function defineRefPropWarningGetter(props, displayName) {
|
||||
{
|
||||
var warnAboutAccessingRef = function() {
|
||||
if (!specialPropRefWarningShown) {
|
||||
specialPropRefWarningShown = true;
|
||||
error("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
|
||||
}
|
||||
};
|
||||
warnAboutAccessingRef.isReactWarning = true;
|
||||
Object.defineProperty(props, "ref", {
|
||||
get: warnAboutAccessingRef,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
}
|
||||
var ReactElement = function(type, key, ref, self, source, owner, props) {
|
||||
var element = {
|
||||
// This tag allows us to uniquely identify this as a React Element
|
||||
$$typeof: REACT_ELEMENT_TYPE,
|
||||
// Built-in properties that belong on the element
|
||||
type,
|
||||
key,
|
||||
ref,
|
||||
props,
|
||||
// Record the component responsible for creating this element.
|
||||
_owner: owner
|
||||
};
|
||||
{
|
||||
element._store = {};
|
||||
Object.defineProperty(element._store, "validated", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: false
|
||||
});
|
||||
Object.defineProperty(element, "_self", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: self
|
||||
});
|
||||
Object.defineProperty(element, "_source", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: source
|
||||
});
|
||||
if (Object.freeze) {
|
||||
Object.freeze(element.props);
|
||||
Object.freeze(element);
|
||||
}
|
||||
}
|
||||
return element;
|
||||
};
|
||||
function jsxDEV(type, config, maybeKey, source, self) {
|
||||
{
|
||||
var propName;
|
||||
var props = {};
|
||||
var key = null;
|
||||
var ref = null;
|
||||
if (maybeKey !== void 0) {
|
||||
{
|
||||
checkKeyStringCoercion(maybeKey);
|
||||
}
|
||||
key = "" + maybeKey;
|
||||
}
|
||||
if (hasValidKey(config)) {
|
||||
{
|
||||
checkKeyStringCoercion(config.key);
|
||||
}
|
||||
key = "" + config.key;
|
||||
}
|
||||
if (hasValidRef(config)) {
|
||||
ref = config.ref;
|
||||
warnIfStringRefCannotBeAutoConverted(config, self);
|
||||
}
|
||||
for (propName in config) {
|
||||
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
|
||||
props[propName] = config[propName];
|
||||
}
|
||||
}
|
||||
if (type && type.defaultProps) {
|
||||
var defaultProps = type.defaultProps;
|
||||
for (propName in defaultProps) {
|
||||
if (props[propName] === void 0) {
|
||||
props[propName] = defaultProps[propName];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key || ref) {
|
||||
var displayName = typeof type === "function" ? type.displayName || type.name || "Unknown" : type;
|
||||
if (key) {
|
||||
defineKeyPropWarningGetter(props, displayName);
|
||||
}
|
||||
if (ref) {
|
||||
defineRefPropWarningGetter(props, displayName);
|
||||
}
|
||||
}
|
||||
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
|
||||
}
|
||||
}
|
||||
var ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner;
|
||||
var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;
|
||||
function setCurrentlyValidatingElement$1(element) {
|
||||
{
|
||||
if (element) {
|
||||
var owner = element._owner;
|
||||
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
|
||||
ReactDebugCurrentFrame$1.setExtraStackFrame(stack);
|
||||
} else {
|
||||
ReactDebugCurrentFrame$1.setExtraStackFrame(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
var propTypesMisspellWarningShown;
|
||||
{
|
||||
propTypesMisspellWarningShown = false;
|
||||
}
|
||||
function isValidElement(object) {
|
||||
{
|
||||
return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
|
||||
}
|
||||
}
|
||||
function getDeclarationErrorAddendum() {
|
||||
{
|
||||
if (ReactCurrentOwner$1.current) {
|
||||
var name = getComponentNameFromType(ReactCurrentOwner$1.current.type);
|
||||
if (name) {
|
||||
return "\n\nCheck the render method of `" + name + "`.";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function getSourceInfoErrorAddendum(source) {
|
||||
{
|
||||
if (source !== void 0) {
|
||||
var fileName = source.fileName.replace(/^.*[\\\/]/, "");
|
||||
var lineNumber = source.lineNumber;
|
||||
return "\n\nCheck your code at " + fileName + ":" + lineNumber + ".";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
var ownerHasKeyUseWarning = {};
|
||||
function getCurrentComponentErrorInfo(parentType) {
|
||||
{
|
||||
var info = getDeclarationErrorAddendum();
|
||||
if (!info) {
|
||||
var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name;
|
||||
if (parentName) {
|
||||
info = "\n\nCheck the top-level render call using <" + parentName + ">.";
|
||||
}
|
||||
}
|
||||
return info;
|
||||
}
|
||||
}
|
||||
function validateExplicitKey(element, parentType) {
|
||||
{
|
||||
if (!element._store || element._store.validated || element.key != null) {
|
||||
return;
|
||||
}
|
||||
element._store.validated = true;
|
||||
var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
|
||||
if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {
|
||||
return;
|
||||
}
|
||||
ownerHasKeyUseWarning[currentComponentErrorInfo] = true;
|
||||
var childOwner = "";
|
||||
if (element && element._owner && element._owner !== ReactCurrentOwner$1.current) {
|
||||
childOwner = " It was passed a child from " + getComponentNameFromType(element._owner.type) + ".";
|
||||
}
|
||||
setCurrentlyValidatingElement$1(element);
|
||||
error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner);
|
||||
setCurrentlyValidatingElement$1(null);
|
||||
}
|
||||
}
|
||||
function validateChildKeys(node, parentType) {
|
||||
{
|
||||
if (typeof node !== "object") {
|
||||
return;
|
||||
}
|
||||
if (isArray(node)) {
|
||||
for (var i = 0; i < node.length; i++) {
|
||||
var child = node[i];
|
||||
if (isValidElement(child)) {
|
||||
validateExplicitKey(child, parentType);
|
||||
}
|
||||
}
|
||||
} else if (isValidElement(node)) {
|
||||
if (node._store) {
|
||||
node._store.validated = true;
|
||||
}
|
||||
} else if (node) {
|
||||
var iteratorFn = getIteratorFn(node);
|
||||
if (typeof iteratorFn === "function") {
|
||||
if (iteratorFn !== node.entries) {
|
||||
var iterator = iteratorFn.call(node);
|
||||
var step;
|
||||
while (!(step = iterator.next()).done) {
|
||||
if (isValidElement(step.value)) {
|
||||
validateExplicitKey(step.value, parentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function validatePropTypes(element) {
|
||||
{
|
||||
var type = element.type;
|
||||
if (type === null || type === void 0 || typeof type === "string") {
|
||||
return;
|
||||
}
|
||||
var propTypes;
|
||||
if (typeof type === "function") {
|
||||
propTypes = type.propTypes;
|
||||
} else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here.
|
||||
// Inner props are checked in the reconciler.
|
||||
type.$$typeof === REACT_MEMO_TYPE)) {
|
||||
propTypes = type.propTypes;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (propTypes) {
|
||||
var name = getComponentNameFromType(type);
|
||||
checkPropTypes(propTypes, element.props, "prop", name, element);
|
||||
} else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) {
|
||||
propTypesMisspellWarningShown = true;
|
||||
var _name = getComponentNameFromType(type);
|
||||
error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown");
|
||||
}
|
||||
if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) {
|
||||
error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead.");
|
||||
}
|
||||
}
|
||||
}
|
||||
function validateFragmentProps(fragment) {
|
||||
{
|
||||
var keys = Object.keys(fragment.props);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
if (key !== "children" && key !== "key") {
|
||||
setCurrentlyValidatingElement$1(fragment);
|
||||
error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key);
|
||||
setCurrentlyValidatingElement$1(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fragment.ref !== null) {
|
||||
setCurrentlyValidatingElement$1(fragment);
|
||||
error("Invalid attribute `ref` supplied to `React.Fragment`.");
|
||||
setCurrentlyValidatingElement$1(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
var didWarnAboutKeySpread = {};
|
||||
function jsxWithValidation(type, props, key, isStaticChildren, source, self) {
|
||||
{
|
||||
var validType = isValidElementType(type);
|
||||
if (!validType) {
|
||||
var info = "";
|
||||
if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) {
|
||||
info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.";
|
||||
}
|
||||
var sourceInfo = getSourceInfoErrorAddendum(source);
|
||||
if (sourceInfo) {
|
||||
info += sourceInfo;
|
||||
} else {
|
||||
info += getDeclarationErrorAddendum();
|
||||
}
|
||||
var typeString;
|
||||
if (type === null) {
|
||||
typeString = "null";
|
||||
} else if (isArray(type)) {
|
||||
typeString = "array";
|
||||
} else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) {
|
||||
typeString = "<" + (getComponentNameFromType(type.type) || "Unknown") + " />";
|
||||
info = " Did you accidentally export a JSX literal instead of a component?";
|
||||
} else {
|
||||
typeString = typeof type;
|
||||
}
|
||||
error("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info);
|
||||
}
|
||||
var element = jsxDEV(type, props, key, source, self);
|
||||
if (element == null) {
|
||||
return element;
|
||||
}
|
||||
if (validType) {
|
||||
var children = props.children;
|
||||
if (children !== void 0) {
|
||||
if (isStaticChildren) {
|
||||
if (isArray(children)) {
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
validateChildKeys(children[i], type);
|
||||
}
|
||||
if (Object.freeze) {
|
||||
Object.freeze(children);
|
||||
}
|
||||
} else {
|
||||
error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
|
||||
}
|
||||
} else {
|
||||
validateChildKeys(children, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
if (hasOwnProperty.call(props, "key")) {
|
||||
var componentName = getComponentNameFromType(type);
|
||||
var keys = Object.keys(props).filter(function(k) {
|
||||
return k !== "key";
|
||||
});
|
||||
var beforeExample = keys.length > 0 ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
|
||||
if (!didWarnAboutKeySpread[componentName + beforeExample]) {
|
||||
var afterExample = keys.length > 0 ? "{" + keys.join(": ..., ") + ": ...}" : "{}";
|
||||
error('A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />', beforeExample, componentName, afterExample, componentName);
|
||||
didWarnAboutKeySpread[componentName + beforeExample] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type === REACT_FRAGMENT_TYPE) {
|
||||
validateFragmentProps(element);
|
||||
} else {
|
||||
validatePropTypes(element);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
var jsxDEV$1 = jsxWithValidation;
|
||||
exports.Fragment = REACT_FRAGMENT_TYPE;
|
||||
exports.jsxDEV = jsxDEV$1;
|
||||
})();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/react/jsx-dev-runtime.js
|
||||
var require_jsx_dev_runtime = __commonJS({
|
||||
"node_modules/react/jsx-dev-runtime.js"(exports, module) {
|
||||
if (false) {
|
||||
module.exports = null;
|
||||
} else {
|
||||
module.exports = require_react_jsx_dev_runtime_development();
|
||||
}
|
||||
}
|
||||
});
|
||||
export default require_jsx_dev_runtime();
|
||||
/*! Bundled license information:
|
||||
|
||||
react/cjs/react-jsx-dev-runtime.development.js:
|
||||
(**
|
||||
* @license React
|
||||
* react-jsx-dev-runtime.development.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*)
|
||||
*/
|
||||
//# sourceMappingURL=react_jsx-dev-runtime.js.map
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../react/cjs/react-jsx-dev-runtime.development.js", "../../react/jsx-dev-runtime.js"],
|
||||
"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": ";;;;;;;;AAAA;AAAA;AAAA;AAYA,QAAI,MAAuC;AACzC,OAAC,WAAW;AACd;AAEA,YAAI,QAAQ;AAMZ,YAAI,qBAAqB,OAAO,IAAI,eAAe;AACnD,YAAI,oBAAoB,OAAO,IAAI,cAAc;AACjD,YAAI,sBAAsB,OAAO,IAAI,gBAAgB;AACrD,YAAI,yBAAyB,OAAO,IAAI,mBAAmB;AAC3D,YAAI,sBAAsB,OAAO,IAAI,gBAAgB;AACrD,YAAI,sBAAsB,OAAO,IAAI,gBAAgB;AACrD,YAAI,qBAAqB,OAAO,IAAI,eAAe;AACnD,YAAI,yBAAyB,OAAO,IAAI,mBAAmB;AAC3D,YAAI,sBAAsB,OAAO,IAAI,gBAAgB;AACrD,YAAI,2BAA2B,OAAO,IAAI,qBAAqB;AAC/D,YAAI,kBAAkB,OAAO,IAAI,YAAY;AAC7C,YAAI,kBAAkB,OAAO,IAAI,YAAY;AAC7C,YAAI,uBAAuB,OAAO,IAAI,iBAAiB;AACvD,YAAI,wBAAwB,OAAO;AACnC,YAAI,uBAAuB;AAC3B,iBAAS,cAAc,eAAe;AACpC,cAAI,kBAAkB,QAAQ,OAAO,kBAAkB,UAAU;AAC/D,mBAAO;AAAA,UACT;AAEA,cAAI,gBAAgB,yBAAyB,cAAc,qBAAqB,KAAK,cAAc,oBAAoB;AAEvH,cAAI,OAAO,kBAAkB,YAAY;AACvC,mBAAO;AAAA,UACT;AAEA,iBAAO;AAAA,QACT;AAEA,YAAI,uBAAuB,MAAM;AAEjC,iBAAS,MAAM,QAAQ;AACrB;AACE;AACE,uBAAS,QAAQ,UAAU,QAAQ,OAAO,IAAI,MAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,GAAG,QAAQ,GAAG,QAAQ,OAAO,SAAS;AACjH,qBAAK,QAAQ,CAAC,IAAI,UAAU,KAAK;AAAA,cACnC;AAEA,2BAAa,SAAS,QAAQ,IAAI;AAAA,YACpC;AAAA,UACF;AAAA,QACF;AAEA,iBAAS,aAAa,OAAO,QAAQ,MAAM;AAGzC;AACE,gBAAIA,0BAAyB,qBAAqB;AAClD,gBAAI,QAAQA,wBAAuB,iBAAiB;AAEpD,gBAAI,UAAU,IAAI;AAChB,wBAAU;AACV,qBAAO,KAAK,OAAO,CAAC,KAAK,CAAC;AAAA,YAC5B;AAGA,gBAAI,iBAAiB,KAAK,IAAI,SAAU,MAAM;AAC5C,qBAAO,OAAO,IAAI;AAAA,YACpB,CAAC;AAED,2BAAe,QAAQ,cAAc,MAAM;AAI3C,qBAAS,UAAU,MAAM,KAAK,QAAQ,KAAK,GAAG,SAAS,cAAc;AAAA,UACvE;AAAA,QACF;AAIA,YAAI,iBAAiB;AACrB,YAAI,qBAAqB;AACzB,YAAI,0BAA0B;AAE9B,YAAI,qBAAqB;AAIzB,YAAI,qBAAqB;AAEzB,YAAI;AAEJ;AACE,mCAAyB,OAAO,IAAI,wBAAwB;AAAA,QAC9D;AAEA,iBAAS,mBAAmB,MAAM;AAChC,cAAI,OAAO,SAAS,YAAY,OAAO,SAAS,YAAY;AAC1D,mBAAO;AAAA,UACT;AAGA,cAAI,SAAS,uBAAuB,SAAS,uBAAuB,sBAAuB,SAAS,0BAA0B,SAAS,uBAAuB,SAAS,4BAA4B,sBAAuB,SAAS,wBAAwB,kBAAmB,sBAAuB,yBAA0B;AAC7T,mBAAO;AAAA,UACT;AAEA,cAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,gBAAI,KAAK,aAAa,mBAAmB,KAAK,aAAa,mBAAmB,KAAK,aAAa,uBAAuB,KAAK,aAAa,sBAAsB,KAAK,aAAa;AAAA;AAAA;AAAA;AAAA,YAIjL,KAAK,aAAa,0BAA0B,KAAK,gBAAgB,QAAW;AAC1E,qBAAO;AAAA,YACT;AAAA,UACF;AAEA,iBAAO;AAAA,QACT;AAEA,iBAAS,eAAe,WAAW,WAAW,aAAa;AACzD,cAAI,cAAc,UAAU;AAE5B,cAAI,aAAa;AACf,mBAAO;AAAA,UACT;AAEA,cAAI,eAAe,UAAU,eAAe,UAAU,QAAQ;AAC9D,iBAAO,iBAAiB,KAAK,cAAc,MAAM,eAAe,MAAM;AAAA,QACxE;AAGA,iBAAS,eAAe,MAAM;AAC5B,iBAAO,KAAK,eAAe;AAAA,QAC7B;AAGA,iBAAS,yBAAyB,MAAM;AACtC,cAAI,QAAQ,MAAM;AAEhB,mBAAO;AAAA,UACT;AAEA;AACE,gBAAI,OAAO,KAAK,QAAQ,UAAU;AAChC,oBAAM,mHAAwH;AAAA,YAChI;AAAA,UACF;AAEA,cAAI,OAAO,SAAS,YAAY;AAC9B,mBAAO,KAAK,eAAe,KAAK,QAAQ;AAAA,UAC1C;AAEA,cAAI,OAAO,SAAS,UAAU;AAC5B,mBAAO;AAAA,UACT;AAEA,kBAAQ,MAAM;AAAA,YACZ,KAAK;AACH,qBAAO;AAAA,YAET,KAAK;AACH,qBAAO;AAAA,YAET,KAAK;AACH,qBAAO;AAAA,YAET,KAAK;AACH,qBAAO;AAAA,YAET,KAAK;AACH,qBAAO;AAAA,YAET,KAAK;AACH,qBAAO;AAAA,UAEX;AAEA,cAAI,OAAO,SAAS,UAAU;AAC5B,oBAAQ,KAAK,UAAU;AAAA,cACrB,KAAK;AACH,oBAAI,UAAU;AACd,uBAAO,eAAe,OAAO,IAAI;AAAA,cAEnC,KAAK;AACH,oBAAI,WAAW;AACf,uBAAO,eAAe,SAAS,QAAQ,IAAI;AAAA,cAE7C,KAAK;AACH,uBAAO,eAAe,MAAM,KAAK,QAAQ,YAAY;AAAA,cAEvD,KAAK;AACH,oBAAI,YAAY,KAAK,eAAe;AAEpC,oBAAI,cAAc,MAAM;AACtB,yBAAO;AAAA,gBACT;AAEA,uBAAO,yBAAyB,KAAK,IAAI,KAAK;AAAA,cAEhD,KAAK,iBACH;AACE,oBAAI,gBAAgB;AACpB,oBAAI,UAAU,cAAc;AAC5B,oBAAI,OAAO,cAAc;AAEzB,oBAAI;AACF,yBAAO,yBAAyB,KAAK,OAAO,CAAC;AAAA,gBAC/C,SAAS,GAAG;AACV,yBAAO;AAAA,gBACT;AAAA,cACF;AAAA,YAGJ;AAAA,UACF;AAEA,iBAAO;AAAA,QACT;AAEA,YAAI,SAAS,OAAO;AAMpB,YAAI,gBAAgB;AACpB,YAAI;AACJ,YAAI;AACJ,YAAI;AACJ,YAAI;AACJ,YAAI;AACJ,YAAI;AACJ,YAAI;AAEJ,iBAAS,cAAc;AAAA,QAAC;AAExB,oBAAY,qBAAqB;AACjC,iBAAS,cAAc;AACrB;AACE,gBAAI,kBAAkB,GAAG;AAEvB,wBAAU,QAAQ;AAClB,yBAAW,QAAQ;AACnB,yBAAW,QAAQ;AACnB,0BAAY,QAAQ;AACpB,0BAAY,QAAQ;AACpB,mCAAqB,QAAQ;AAC7B,6BAAe,QAAQ;AAEvB,kBAAI,QAAQ;AAAA,gBACV,cAAc;AAAA,gBACd,YAAY;AAAA,gBACZ,OAAO;AAAA,gBACP,UAAU;AAAA,cACZ;AAEA,qBAAO,iBAAiB,SAAS;AAAA,gBAC/B,MAAM;AAAA,gBACN,KAAK;AAAA,gBACL,MAAM;AAAA,gBACN,OAAO;AAAA,gBACP,OAAO;AAAA,gBACP,gBAAgB;AAAA,gBAChB,UAAU;AAAA,cACZ,CAAC;AAAA,YAEH;AAEA;AAAA,UACF;AAAA,QACF;AACA,iBAAS,eAAe;AACtB;AACE;AAEA,gBAAI,kBAAkB,GAAG;AAEvB,kBAAI,QAAQ;AAAA,gBACV,cAAc;AAAA,gBACd,YAAY;AAAA,gBACZ,UAAU;AAAA,cACZ;AAEA,qBAAO,iBAAiB,SAAS;AAAA,gBAC/B,KAAK,OAAO,CAAC,GAAG,OAAO;AAAA,kBACrB,OAAO;AAAA,gBACT,CAAC;AAAA,gBACD,MAAM,OAAO,CAAC,GAAG,OAAO;AAAA,kBACtB,OAAO;AAAA,gBACT,CAAC;AAAA,gBACD,MAAM,OAAO,CAAC,GAAG,OAAO;AAAA,kBACtB,OAAO;AAAA,gBACT,CAAC;AAAA,gBACD,OAAO,OAAO,CAAC,GAAG,OAAO;AAAA,kBACvB,OAAO;AAAA,gBACT,CAAC;AAAA,gBACD,OAAO,OAAO,CAAC,GAAG,OAAO;AAAA,kBACvB,OAAO;AAAA,gBACT,CAAC;AAAA,gBACD,gBAAgB,OAAO,CAAC,GAAG,OAAO;AAAA,kBAChC,OAAO;AAAA,gBACT,CAAC;AAAA,gBACD,UAAU,OAAO,CAAC,GAAG,OAAO;AAAA,kBAC1B,OAAO;AAAA,gBACT,CAAC;AAAA,cACH,CAAC;AAAA,YAEH;AAEA,gBAAI,gBAAgB,GAAG;AACrB,oBAAM,8EAAmF;AAAA,YAC3F;AAAA,UACF;AAAA,QACF;AAEA,YAAI,yBAAyB,qBAAqB;AAClD,YAAI;AACJ,iBAAS,8BAA8B,MAAM,QAAQ,SAAS;AAC5D;AACE,gBAAI,WAAW,QAAW;AAExB,kBAAI;AACF,sBAAM,MAAM;AAAA,cACd,SAAS,GAAG;AACV,oBAAI,QAAQ,EAAE,MAAM,KAAK,EAAE,MAAM,cAAc;AAC/C,yBAAS,SAAS,MAAM,CAAC,KAAK;AAAA,cAChC;AAAA,YACF;AAGA,mBAAO,OAAO,SAAS;AAAA,UACzB;AAAA,QACF;AACA,YAAI,UAAU;AACd,YAAI;AAEJ;AACE,cAAI,kBAAkB,OAAO,YAAY,aAAa,UAAU;AAChE,gCAAsB,IAAI,gBAAgB;AAAA,QAC5C;AAEA,iBAAS,6BAA6B,IAAI,WAAW;AAEnD,cAAK,CAAC,MALine truncated
|
||||
"names": ["ReactDebugCurrentFrame"]
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import {
|
||||
require_jsx_runtime
|
||||
} from "./chunk-6PXSGDAH.js";
|
||||
import "./chunk-DRWLMN53.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_jsx_runtime();
|
||||
//# sourceMappingURL=react_jsx-runtime.js.map
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
# @babel/code-frame
|
||||
|
||||
> Generate errors that contain a code frame that point to source locations.
|
||||
|
||||
See our website [@babel/code-frame](https://babeljs.io/docs/babel-code-frame) for more information.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save-dev @babel/code-frame
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/code-frame --dev
|
||||
```
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var picocolors = require('picocolors');
|
||||
var jsTokens = require('js-tokens');
|
||||
var helperValidatorIdentifier = require('@babel/helper-validator-identifier');
|
||||
|
||||
function isColorSupported() {
|
||||
return (typeof process === "object" && (process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false") ? false : picocolors.isColorSupported
|
||||
);
|
||||
}
|
||||
const compose = (f, g) => v => f(g(v));
|
||||
function buildDefs(colors) {
|
||||
return {
|
||||
keyword: colors.cyan,
|
||||
capitalized: colors.yellow,
|
||||
jsxIdentifier: colors.yellow,
|
||||
punctuator: colors.yellow,
|
||||
number: colors.magenta,
|
||||
string: colors.green,
|
||||
regex: colors.magenta,
|
||||
comment: colors.gray,
|
||||
invalid: compose(compose(colors.white, colors.bgRed), colors.bold),
|
||||
gutter: colors.gray,
|
||||
marker: compose(colors.red, colors.bold),
|
||||
message: compose(colors.red, colors.bold),
|
||||
reset: colors.reset
|
||||
};
|
||||
}
|
||||
const defsOn = buildDefs(picocolors.createColors(true));
|
||||
const defsOff = buildDefs(picocolors.createColors(false));
|
||||
function getDefs(enabled) {
|
||||
return enabled ? defsOn : defsOff;
|
||||
}
|
||||
|
||||
const sometimesKeywords = new Set(["as", "async", "from", "get", "of", "set"]);
|
||||
const NEWLINE$1 = /\r\n|[\n\r\u2028\u2029]/;
|
||||
const BRACKET = /^[()[\]{}]$/;
|
||||
let tokenize;
|
||||
{
|
||||
const JSX_TAG = /^[a-z][\w-]*$/i;
|
||||
const getTokenType = function (token, offset, text) {
|
||||
if (token.type === "name") {
|
||||
if (helperValidatorIdentifier.isKeyword(token.value) || helperValidatorIdentifier.isStrictReservedWord(token.value, true) || sometimesKeywords.has(token.value)) {
|
||||
return "keyword";
|
||||
}
|
||||
if (JSX_TAG.test(token.value) && (text[offset - 1] === "<" || text.slice(offset - 2, offset) === "</")) {
|
||||
return "jsxIdentifier";
|
||||
}
|
||||
if (token.value[0] !== token.value[0].toLowerCase()) {
|
||||
return "capitalized";
|
||||
}
|
||||
}
|
||||
if (token.type === "punctuator" && BRACKET.test(token.value)) {
|
||||
return "bracket";
|
||||
}
|
||||
if (token.type === "invalid" && (token.value === "@" || token.value === "#")) {
|
||||
return "punctuator";
|
||||
}
|
||||
return token.type;
|
||||
};
|
||||
tokenize = function* (text) {
|
||||
let match;
|
||||
while (match = jsTokens.default.exec(text)) {
|
||||
const token = jsTokens.matchToToken(match);
|
||||
yield {
|
||||
type: getTokenType(token, match.index, text),
|
||||
value: token.value
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
function highlight(text) {
|
||||
if (text === "") return "";
|
||||
const defs = getDefs(true);
|
||||
let highlighted = "";
|
||||
for (const {
|
||||
type,
|
||||
value
|
||||
} of tokenize(text)) {
|
||||
if (type in defs) {
|
||||
highlighted += value.split(NEWLINE$1).map(str => defs[type](str)).join("\n");
|
||||
} else {
|
||||
highlighted += value;
|
||||
}
|
||||
}
|
||||
return highlighted;
|
||||
}
|
||||
|
||||
let deprecationWarningShown = false;
|
||||
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
|
||||
function getMarkerLines(loc, source, opts) {
|
||||
const startLoc = Object.assign({
|
||||
column: 0,
|
||||
line: -1
|
||||
}, loc.start);
|
||||
const endLoc = Object.assign({}, startLoc, loc.end);
|
||||
const {
|
||||
linesAbove = 2,
|
||||
linesBelow = 3
|
||||
} = opts || {};
|
||||
const startLine = startLoc.line;
|
||||
const startColumn = startLoc.column;
|
||||
const endLine = endLoc.line;
|
||||
const endColumn = endLoc.column;
|
||||
let start = Math.max(startLine - (linesAbove + 1), 0);
|
||||
let end = Math.min(source.length, endLine + linesBelow);
|
||||
if (startLine === -1) {
|
||||
start = 0;
|
||||
}
|
||||
if (endLine === -1) {
|
||||
end = source.length;
|
||||
}
|
||||
const lineDiff = endLine - startLine;
|
||||
const markerLines = {};
|
||||
if (lineDiff) {
|
||||
for (let i = 0; i <= lineDiff; i++) {
|
||||
const lineNumber = i + startLine;
|
||||
if (!startColumn) {
|
||||
markerLines[lineNumber] = true;
|
||||
} else if (i === 0) {
|
||||
const sourceLength = source[lineNumber - 1].length;
|
||||
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
|
||||
} else if (i === lineDiff) {
|
||||
markerLines[lineNumber] = [0, endColumn];
|
||||
} else {
|
||||
const sourceLength = source[lineNumber - i].length;
|
||||
markerLines[lineNumber] = [0, sourceLength];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (startColumn === endColumn) {
|
||||
if (startColumn) {
|
||||
markerLines[startLine] = [startColumn, 0];
|
||||
} else {
|
||||
markerLines[startLine] = true;
|
||||
}
|
||||
} else {
|
||||
markerLines[startLine] = [startColumn, endColumn - startColumn];
|
||||
}
|
||||
}
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
markerLines
|
||||
};
|
||||
}
|
||||
function codeFrameColumns(rawLines, loc, opts = {}) {
|
||||
const shouldHighlight = opts.forceColor || isColorSupported() && opts.highlightCode;
|
||||
const defs = getDefs(shouldHighlight);
|
||||
const lines = rawLines.split(NEWLINE);
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
markerLines
|
||||
} = getMarkerLines(loc, lines, opts);
|
||||
const hasColumns = loc.start && typeof loc.start.column === "number";
|
||||
const numberMaxWidth = String(end).length;
|
||||
const highlightedLines = shouldHighlight ? highlight(rawLines) : rawLines;
|
||||
let frame = highlightedLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
|
||||
const number = start + 1 + index;
|
||||
const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
|
||||
const gutter = ` ${paddedNumber} |`;
|
||||
const hasMarker = markerLines[number];
|
||||
const lastMarkerLine = !markerLines[number + 1];
|
||||
if (hasMarker) {
|
||||
let markerLine = "";
|
||||
if (Array.isArray(hasMarker)) {
|
||||
const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
|
||||
const numberOfMarkers = hasMarker[1] || 1;
|
||||
markerLine = ["\n ", defs.gutter(gutter.replace(/\d/g, " ")), " ", markerSpacing, defs.marker("^").repeat(numberOfMarkers)].join("");
|
||||
if (lastMarkerLine && opts.message) {
|
||||
markerLine += " " + defs.message(opts.message);
|
||||
}
|
||||
}
|
||||
return [defs.marker(">"), defs.gutter(gutter), line.length > 0 ? ` ${line}` : "", markerLine].join("");
|
||||
} else {
|
||||
return ` ${defs.gutter(gutter)}${line.length > 0 ? ` ${line}` : ""}`;
|
||||
}
|
||||
}).join("\n");
|
||||
if (opts.message && !hasColumns) {
|
||||
frame = `${" ".repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
|
||||
}
|
||||
if (shouldHighlight) {
|
||||
return defs.reset(frame);
|
||||
} else {
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
function index (rawLines, lineNumber, colNumber, opts = {}) {
|
||||
if (!deprecationWarningShown) {
|
||||
deprecationWarningShown = true;
|
||||
const message = "Passing lineNumber and colNumber is deprecated to @babel/code-frame. Please use `codeFrameColumns`.";
|
||||
if (process.emitWarning) {
|
||||
process.emitWarning(message, "DeprecationWarning");
|
||||
} else {
|
||||
const deprecationError = new Error(message);
|
||||
deprecationError.name = "DeprecationWarning";
|
||||
console.warn(new Error(message));
|
||||
}
|
||||
}
|
||||
colNumber = Math.max(colNumber, 0);
|
||||
const location = {
|
||||
start: {
|
||||
column: colNumber,
|
||||
line: lineNumber
|
||||
}
|
||||
};
|
||||
return codeFrameColumns(rawLines, location, opts);
|
||||
}
|
||||
|
||||
exports.codeFrameColumns = codeFrameColumns;
|
||||
exports.default = index;
|
||||
exports.highlight = highlight;
|
||||
//# sourceMappingURL=index.js.map
|
||||
-1
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sources":["../src/defs.ts","../src/highlight.ts","../src/index.ts"],"sourcesContent":["import picocolors, { createColors } from \"picocolors\";\nimport type { Colors, Formatter } from \"picocolors/types\";\n\nexport function isColorSupported() {\n return (\n // See https://github.com/alexeyraspopov/picocolors/issues/62\n typeof process === \"object\" &&\n (process.env.FORCE_COLOR === \"0\" || process.env.FORCE_COLOR === \"false\")\n ? false\n : picocolors.isColorSupported\n );\n}\n\nexport type InternalTokenType =\n | \"keyword\"\n | \"capitalized\"\n | \"jsxIdentifier\"\n | \"punctuator\"\n | \"number\"\n | \"string\"\n | \"regex\"\n | \"comment\"\n | \"invalid\";\n\ntype UITokens = \"gutter\" | \"marker\" | \"message\";\n\nexport type Defs = {\n [_ in InternalTokenType | UITokens | \"reset\"]: Formatter;\n};\n\nconst compose: <T, U, V>(f: (gv: U) => V, g: (v: T) => U) => (v: T) => V =\n (f, g) => v =>\n f(g(v));\n\n/**\n * Styles for token types.\n */\nfunction buildDefs(colors: Colors): Defs {\n return {\n keyword: colors.cyan,\n capitalized: colors.yellow,\n jsxIdentifier: colors.yellow,\n punctuator: colors.yellow,\n number: colors.magenta,\n string: colors.green,\n regex: colors.magenta,\n comment: colors.gray,\n invalid: compose(compose(colors.white, colors.bgRed), colors.bold),\n\n gutter: colors.gray,\n marker: compose(colors.red, colors.bold),\n message: compose(colors.red, colors.bold),\n\n reset: colors.reset,\n };\n}\n\nconst defsOn = buildDefs(createColors(true));\nconst defsOff = buildDefs(createColors(false));\n\nexport function getDefs(enabled: boolean): Defs {\n return enabled ? defsOn : defsOff;\n}\n","import type { Token as JSToken, JSXToken } from \"js-tokens\";\nimport jsTokens from \"js-tokens\";\n\nimport {\n isStrictReservedWord,\n isKeyword,\n} from \"@babel/helper-validator-identifier\";\n\nimport { getDefs, type InternalTokenType } from \"./defs.ts\";\n\n/**\n * Names that are always allowed as identifiers, but also appear as keywords\n * within certain syntactic productions.\n *\n * https://tc39.es/ecma262/#sec-keywords-and-reserved-words\n *\n * `target` has been omitted since it is very likely going to be a false\n * positive.\n */\nconst sometimesKeywords = new Set([\"as\", \"async\", \"from\", \"get\", \"of\", \"set\"]);\n\ntype Token = {\n type: InternalTokenType | \"uncolored\";\n value: string;\n};\n\n/**\n * RegExp to test for newlines in terminal.\n */\nconst NEWLINE = /\\r\\n|[\\n\\r\\u2028\\u2029]/;\n\n/**\n * RegExp to test for the three types of brackets.\n */\nconst BRACKET = /^[()[\\]{}]$/;\n\nlet tokenize: (\n text: string,\n) => Generator<{ type: InternalTokenType | \"uncolored\"; value: string }>;\n\nif (process.env.BABEL_8_BREAKING) {\n /**\n * Get the type of token, specifying punctuator type.\n */\n const getTokenType = function (\n token: JSToken | JSXToken,\n ): InternalTokenType | \"uncolored\" {\n if (token.type === \"IdentifierName\") {\n if (\n isKeyword(token.value) ||\n isStrictReservedWord(token.value, true) ||\n sometimesKeywords.has(token.value)\n ) {\n return \"keyword\";\n }\n\n if (token.value[0] !== token.value[0].toLowerCase()) {\n return \"capitalized\";\n }\n }\n\n if (token.type === \"Punctuator\" && BRACKET.test(token.value)) {\n return \"uncolored\";\n }\n\n if (token.type === \"Invalid\" && token.value === \"@\") {\n return \"punctuator\";\n }\n\n switch (token.type) {\n case \"NumericLiteral\":\n return \"number\";\n\n case \"StringLiteral\":\n case \"JSXString\":\n case \"NoSubstitutionTemplate\":\n return \"string\";\n\n case \"RegularExpressionLiteral\":\n return \"regex\";\n\n case \"Punctuator\":\n case \"JSXPunctuator\":\n return \"punctuator\";\n\n case \"MultiLineComment\":\n case \"SingleLineComment\":\n return \"comment\";\n\n case \"Invalid\":\n case \"JSXInvalid\":\n return \"invalid\";\n\n case \"JSXIdentifier\":\n return \"jsxIdentifier\";\n\n default:\n return \"uncolored\";\n }\n };\n\n /**\n * Turn a string of JS into an array of objects.\n */\n tokenize = function* (text: string): Generator<Token> {\n for (const token of jsTokens(text, { jsx: true })) {\n switch (token.type) {\n case \"TemplateHead\":\n yield { type: \"string\", value: token.value.slice(0, -2) };\n yield { type: \"punctuator\", value: \"${\" };\n break;\n\n case \"TemplateMiddle\":\n yield { type: \"punctuator\", value: \"}\" };\n yield { type: \"string\", value: token.value.slice(1, -2) };\n yield { type: \"punctuator\", value: \"${\" };\n break;\n\n case \"TemplateTail\":\n yield { type: \"punctuator\", valuLine truncated
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@babel/code-frame",
|
||||
"version": "7.27.1",
|
||||
"description": "Generate errors that contain a code frame that point to source locations.",
|
||||
"author": "The Babel Team (https://babel.dev/team)",
|
||||
"homepage": "https://babel.dev/docs/en/next/babel-code-frame",
|
||||
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen",
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/babel/babel.git",
|
||||
"directory": "packages/babel-code-frame"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"import-meta-resolve": "^4.1.0",
|
||||
"strip-ansi": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"type": "commonjs"
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
# @babel/compat-data
|
||||
|
||||
> The compat-data to determine required Babel plugins
|
||||
|
||||
See our website [@babel/compat-data](https://babeljs.io/docs/babel-compat-data) for more information.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save @babel/compat-data
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add @babel/compat-data
|
||||
```
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Todo (Babel 8): remove this file as Babel 8 drop support of core-js 2
|
||||
module.exports = require("./data/corejs2-built-ins.json");
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Todo (Babel 8): remove this file now that it is included in babel-plugin-polyfill-corejs3
|
||||
module.exports = require("./data/corejs3-shipped-proposals.json");
|
||||
Loaded 100 of 9268 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user