# Anforderungen – AufmaßWeb v2.35

## Projektübersicht
Flask-basierte Webanwendung zur Verwaltung von Aufmaßen, Projekten und Positionen.

---

## 1. Projektübersicht (`/projekt/`)

### 1.1 Sortierung
- Projekte werden nach **Bezeichnung A-Z** aufsteigend sortiert angezeigt
- NULL-Werte werden als leerer String behandelt (sortieren ans Ende)

### 1.2 Projektsuche
- Suchleiste oben auf der Seite
- Clientseitige Filterung nach Projektname (Bezeichnung / SM-Nr)
- Echtzeit-Filterung bei Eingabe, Clear-Button vorhanden

### 1.3 Baumansicht
- Jedes Projekt als aufklappbarer `<details>`-Block
- Zeigt: Bezeichnung, Summe (€), Anzahl Aufmaße, Anzahl Positionen, Status
- Doppelklick auf Projekt-Header navigiert zur Listenansicht (`/projekt/<id>`)

### 1.4 Aufmaß-Suche
- Suchleiste filtert Aufmaß-Namen innerhalb der Baumansicht

### 1.5 Inline-Bearbeitung Aufmaß-Name
- ✏️-Button pro Aufmaß → Inline-Input für Name
- Speichern: Blur oder Enter
- Abbrechen: Escape
- Kein OK/Abbrechen-Button, kein setTimeout

### 1.6 Inline-Bearbeitung Projekt-Name
- ✏️-Button im Projekt-Header → Inline-Input für Name
- Speichern: Blur oder Enter
- Abbrechen: Escape
- POST an `/projekt/<id>/update-name`

### 1.7 Aufmaß löschen
- ✕-Button pro Aufmaß, keine Bestätigung (ohne `confirm()`)
- Redirect per `request.referrer`: in Baumansicht → bleibt in `index`, in Detailansicht → bleibt in `list`

### 1.8 Aufmaß duplizieren
- Duplizieren-Button (📋) pro Aufmaß, keine Bestätigung
- Erstellt Kopie mit allen Positionen, hängt ` (Kopie)` an den Namen
- Bei Namenskonflikt: automatisch ` (1)`, ` (2)` etc.
- Redirect per `request.referrer` (wie Löschen)

### 1.9 Baum-Status persistent
- `<details>` open/close-Zustand in `localStorage`
- Schlüssel: `tree_open_{projectId}`
- Überlebt Seitenneuladungen nach Aktionen

---

## 2. Neues Aufmaß erstellen

### 2.1 Button
- **"+ Neues Aufmaß"**-Button unter jedem Projekt in der Baumansicht
- Klick klappt ein vollständiges Formular auf (slide-down)

### 2.2 Formular-Felder (Reihenfolge)
1. **Vertrag** (Dropdown, optional)
2. **LV-Name** (Text, optional)
3. **Typ** (Dropdown, optional)
4. **Bezeichnung (Baustelle)** (Text, leer)
5. **Bauabschnitt** (Text, pre-filled aus Projekt)
6. **SM-Nr** (Text, pre-filled aus Projekt)
7. **Abruf-Nr** (Text, pre-filled aus Projekt)
8. **Kopfdaten EV holen** (Button, bedingt sichtbar – siehe Abschnitt 11)
9. **Ansprechpartner** (Trennlinie)
   - Vorname
   - Name
   - Tel
   - Email
10. **Startdatum** (Date)
11. **Endedatum** (Date)
12. **Aufmaß-Name** (readonly, automatisch generiert)

### 2.3 Automatische Aufmaß-Namensgenerierung
- Zusammensetzung: `Bezeichnung + " - " + Bauabschnitt + " - " + SM-Nr + " - " + Abruf-Nr`
- Nur nicht-leere Felder werden berücksichtigt
- Wird live bei Eingabe aktualisiert (readonly-Feld)
- Nur OS/Nextcloud-sichere Zeichen: `< > : " / \ | ? * & # % { } ~ [ ]` werden entfernt, Mehrfachleerzeichen → eines

### 2.4 Speichern
- Aktualisiert Projekt-Metadaten (SM-Nr, Vertrag, LV-Name, Abruf-Nr, Daten, Ansprechpartner)
- `project.bezeichnung` wird NICHT überschrieben
- Erstellt neues Aufmaß mit generiertem Namen und gewähltem Typ
- Redirect zur Projektübersicht

---

## 3. Neues Projekt erstellen (`/projekt/neu`)

### 3.1 Formular (vereinfacht)
- **Projektname / Bezeichnung** (Pflichtfeld)
- **Vertrag** (Dropdown, optional)
- **LV-Name** (Dropdown, optional, abhängig von Vertrag)
- Kein Standard-Aufmaß wird automatisch erstellt

---

## 4. Projekt-Listenansicht (`/projekt/<id>`)

### 4.1 Aufmaß-Liste
- Alle Aufmaße eines Projekts als Tabelle
- Inline-Editing für Name, Typ, Status
- Sofortige visuelle Aktualisierung vor async-fetch
- Kein `resp.status`-Check (Server-Response enthält Aufmaß-Status, nicht 'ok')

### 4.2 Projekt-Name inline bearbeiten
- ✏️-Button in der Projektkopfzeile
- Gleiche Funktionalität wie in Baumansicht (POST `/projekt/<id>/update-name`)

### 4.3 Aufmaß erstellen (in Listenansicht)
- Formular mit Name + Typ + Sortierung

### 4.4 TXT-Import
- 📥 Import-Button → Lädt TXT-Datei
- Parst `[Kopfdaten]` + `[Aufmaßdaten]`
- Erstellt Aufmaß mit Positionen
- Bei doppeltem Namen: automatisch ` (1)`, ` (2)` etc. anfügen

---

## 5. Aufmaß-Editor (`/projekt/<id>/<aufmass_id>`)

### 5.1 Positionstabelle
- Inline-Editing für alle Positionsfelder
- Speichern: Blur oder Enter
- Sofortige visuelle Aktualisierung vor async-fetch
- Kein `resp.status`-Check (Server-Response enthält Aufmaß-Status, nicht 'ok')

### 5.2 Zahlenformat
- Alle Zahleneingaben: `type="text" inputmode="decimal"`
- **Deutsches Format**: Komma = Dezimaltrenner, Punkt = Tausendertrenner
- `germanNum()`: Zahl → deutsche Anzeige (z.B. `1.234,56`)
- `parseNum()`: Deutsche Eingabe → Zahl (z.B. `1.234,56` → `1234.56`)
- `sanitizeNum()`: Entfernt ungültige Zeichen live bei Eingabe
- `validateNumField()`: Rote Border bei ungültigem Format
- **Validierung**: Erlaubt sind `123`, `1.234`, `1,5`, `1.234,56`, `-1,5`
  - Ungültig: Buchstaben, `1,2,3`, `1.2.3`, etc.

### 5.3 ST/LE/STD/h/Psch – Menge-Verhalten
- Faktor-Feld ist immer `1,0` (kein Highlight)
- Bei ST/LE/STD/h/Psch wird **Menge** hervorgehoben (amber `#fff3cd`, fett) und ist **leer** – die Menge ist hier das relevante Feld, muss aber vom Benutzer über die Berechnung (Faktor-Eingabe) bestimmt werden
- Bei M/M2/M3 werden die Dimensionsfelder (Länge/Breite/Tiefe) hervorgehoben
- Menge ist readonly und wird automatisch berechnet: ST/LE/STD/h/Psch → `Faktor × 1`, M → Länge, M2 → L×B, M3 → L×B×T

### 5.4 Formel-Auswahl
- **Standard**: Länge × Breite × Tiefe (abhängig von Einheit)
- **Frei**: Eigenes Formelfeld
- Typabhängige Markierung der Pflichtfelder (Länge, Breite, Tiefe) mit amber

### 5.5 Einfüge-Position ("Am Ende anfügen")
- Checkbox "Am Ende anfügen" (default: checked)
- **checked**: Neue Positionen werden am Ende der Tabelle angefügt
- **unchecked**: Neue Positionen werden unterhalb der zuletzt markierten Position eingefügt
- Klick auf eine Position in der rechten Tabelle setzt die Einfügemarke, ohne die LV-Auswahl zu beeinflussen

### 5.6 LV-Tabelle (linke Seite)
- Spalten: Pos-Nr, Kurztext, EP (deutsch formatiert via `|german_number`), Einheit
- Einfachklick: LV-Zeile markieren, Formular füllen
- **Multi-Select**: Strg+Klick markiert mehrere Zeilen
- **Drag & Drop**: Ziehen einer LV-Zeile überträgt alle markierten IDs (nicht nur die gezogene)
- **Standard-Menge bei Drag**: Nach LV-Drag bleibt menge=0 (keine automatische `berechne_menge()`)
- `fillForm()` setzt `currentLVRow` / `currentLVId` pro Zeile
- Klick auf Positionstabelle löscht NICHT `currentLVRow` oder die LV-Auswahl

### 5.7 Hinzufügen-Button
- **"Hinzufügen"** und **"Auswahl hinzufügen"**: Beide verwenden `selectedSet` (alle markierten LV-Zeilen) + Formularwerte (Overrides)
- Bei Mehrfachauswahl (>1 LV-Zeile) werden Text-Overrides (`pos_nr`, `kurztext`, `abschnitt`, `bemerkung`) **nicht** gesendet – jede Position behält ihre LV-eigenen Textwerte
- Dimensions-Overrides (Faktor, Länge, Breite, Tiefe, Formel) werden immer für alle markierten Positionen verwendet
- Beide nutzen `getInsertIdxFromCheckbox()` für die Einfügeposition

### 5.8 Positionen importieren (TXT)
- 📥 Button "Import TXT" im Tabellen-Footer
- Lädt nur `[Aufmaßdaten]` aus einer TXT-Datei
- Fügt Positionen in das aktuelle Aufmaß ein
- Bestehende Kopfdaten bleiben unverändert

### 5.9 Kontext-Menü (Rechtsklick)
- Rechtsklick auf eine Position in der Tabelle öffnet ein Kontext-Menü
- **Markierte löschen** (✕) – löscht alle markierten Positionen (wie Button unten)
- **Markierte kopieren** (📋) – kopiert alle markierten Positionen (wie Button unten)
- **Leere Zeile einfügen** (📄) – fügt eine leere Zeile unterhalb der angeklickten Position ein
- Beim Rechtsklick ohne Ctrl/Shift wird die angeklickte Zeile zuerst selektiert (Single-Select)
- Rechtsklick auf Trenner/Leerzeilen ist ebenfalls möglich (setzt Einfügepunkt)
- Menü schließt bei Klick außerhalb oder Escape

### 5.10 Spalten-Filter (Excel-ähnlich)
- Jede Spaltenüberschrift (außer Z-Art und ✕) hat einen ▾-Filterbutton
- Klick öffnet ein Dropdown-Menü mit:
  - **↑ Aufsteigend / ↓ Absteigend sortieren** (Sortierung aufheben mit ✕)
  - **Suchen…** (Livesuche filtert Werte-Liste + Tabelle)
  - **Checkbox-Liste** aller vorkommenden Werte mit **Alles**/**Nichts**
- Mehrere Spalten können gleichzeitig gefiltert werden (Und-Verknüpfung)
- Aktive Filter werden durch blaue ▾-Buttons angezeigt
- **↺ Filter zurücksetzen**-Button lädt die Tabelle neu und entfernt alle Filter
- Nur Datenzeilen (mit `data-id`) werden gefiltert – Trenner bleiben sichtbar
- Sortieren + Filtern sind kombinierbar und wirken nur clientseitig (Ansichtsfilter)
- Einfügen, Löschen und Bearbeiten sind auch im gefilterten Zustand möglich

### 5.11 Export (Excel, PDF, TXT) mit Ansichtsfilter
- Buttons: **📊 Excel**, **📄 PDF**, **📄 TXT** in der Kopfzeile
- Checkbox **"Aktuelle Ansicht"** – wenn aktiv, werden nur sichtbare (gefilterte) Positionen exportiert
- TXT-Export erzeugt UTF-8-Datei im Format `[Kopfdaten]` + `[Aufmaßdaten]` (pipe-separiert)
- Export-Route akzeptiert `?visible_ids=1,2,3` zum Filtern

### 5.12 LV-Tabelle Filter
- Gleiches Filter-UI wie Positionstabelle (▾-Buttons in Spaltenüberschriften)
- Spalten: Pos-Nr, Kurztext, EH, EP
- **Sortieren** (↑/↓) und **Suchen** (Text-Livesuche)
- Keine Checkbox-Liste (LV hat zu viele unique values)
- Aktive Filter werden durch blaue ▾-Buttons angezeigt

### 5.13 Spaltenbreite anpassen (ziehen)
- Jede Spaltenüberschrift hat rechts einen unsichtbaren Resize-Handle (5px breit)
- **Ziehen**: Breite ändert sich live, bei Loslassen in `localStorage` gespeichert
- **Doppelklick** auf Handle: automatische optimale Breite (maximale Inhaltsbreite)
- **Rechtsklick** auf Spaltenkopf → Kontext-Menü mit **"💾 Ansicht speichern"**
  - Fixiert die aktuellen Spaltenbreiten als `saved_lv_col_widths` / `saved_pos_col_widths`
  - Nach Seitenneuladung werden gespeicherte Breiten wiederhergestellt
- Funktion `saveColumnWidths()` speichert aktuelle Breiten separat als "gespeicherte Ansicht"
- Gespeicherte Breiten überleben Seitenneuladungen (werden sofort beim Laden angewandt)

### 5.14 Positionstabelle Farben
- `has-background-warning-light` (gelb) nur wenn Wert=0 UND passende Einheit (z.B. Menge=0 bei ST)

### 5.10 LV-Ergebnis EP
- EP-Werte in der LV-Tabelle werden mit deutschem Zahlenformat angezeigt (`|german_number`)

---

## 6. Datenbank

### 6.1 Projekt-Modell (`projekte`)
- `id`, `company_id`, `contract_id`
- `sm_nr`, `bezeichnung`, `vertrag`, `abruf_nr`, `lv_name`
- `datum_start`, `datum_ende`
- `ansprechpartner_vorname`, `ansprechpartner_nachname`, `ansprechpartner_tel`, `ansprechpartner_email`
- `bauabschnitt`, `status`
- `erstellt_von`, `erstellt_am`, `geaendert_am`

### 6.2 Lizenz-Modell (`lizenzen`)
- `uid` (auto-generiert SHA256)
- `max_mitarbeiter`, `max_module_slots`
- `unlimited_users`, `unlimited_modules`
- Properties: `used_users`, `used_module_slots`, `user_slots_display()`, `module_slots_display()`

### 6.3 Datenbank-Engine
- Lokal: SQLite (`data/aufmass.db`)
- Produktion: PostgreSQL via `DATABASE_URL` Umgebungsvariable
- Docker-Setup mit PostgreSQL-Service vorhanden

---

## 7. Docker

### 7.1 Services
- `db`: PostgreSQL 16-alpine mit Healthcheck
- `web`: Flask-App mit Gunicorn (4 Worker)

### 7.2 Konfiguration
- `DATABASE_URL` wird via Environment Variable gesetzt
- `SECRET_KEY` muss vor Produktion geändert werden
- Volume `pgdata` für persistente Daten

---

## 8. Rollen & Berechtigungen

### 8.1 Superadmin
- Zugriff auf alle Projekte, Firmen, Lizenzen
- Firmenverwaltung & Lizenzverwaltung im Menü
- Dashboard: Lizenz-Statistiken, Firmen-Übersicht

### 8.2 Firmadmin
- Zugriff auf Projekte der eigenen Firma
- Mitarbeiterverwaltung, Rechteverwaltung
- Lizenz-Übersicht

### 8.3 Mitarbeiter
- Zugriff nur auf zugewiesene Projekte (ProjectAccess)
- Berechtigungen: lesen, schreiben, aufmass_verwalten, projekte_anlegen, darf_preise_sehen, darf_lv_verwalten

---

## 9. Login-Daten

| Rolle | Email | Passwort |
|---|---|---|
| Superadmin | super@admin.de | admin123 |
| Firmadmin | firmadmin@kpt.de | firma123 |

---

## 10. Wichtige technische Regeln

### 10.1 Inline-Edit
- Blur/Enter speichert, Escape bricht ab
- Keine OK/X-Buttons, kein setTimeout

### 10.2 Visuelle Aktualisierung
- SOFORT vor async-fetch, nicht im Callback

### 10.3 Server-Response (resp.status)
- **`resp.status`** ist das Aufmaß-Statusfeld ('aktiv'), NICHT ein Erfolgsindikator
- **Kein `resp.status !== 'ok'`-Check**

### 10.4 Projekt-Name schützen
- `project.bezeichnung` wird in `aufmass_neu_voll` und `aufmass_import` NICHT überschrieben
- Nur explizit via `POST /projekt/<id>/update-name` änderbar

### 10.5 Redirect-Verhalten
- Löschen, Duplizieren, Import: `request.referrer` prüfen
- Baumansicht (`/projekt/`) → Redirect zu `aufmass.index`
- Detailansicht (`/projekt/<id>`) → Redirect zu `aufmass.aufmass_list`
- Erkennung: `referrer.rstrip('/').endswith('/projekt')`

### 10.6 Namensvalidierung
- Aufmaß-Name: nur OS/Nextcloud-sichere Zeichen
- Entfernt: `< > : " / \ | ? * & # % { } ~ [ ]`
- Mehrfachleerzeichen → eines

---

## 11. E-Vergabe Addon

### 11.1 Firmeneinstellungen (Firmadmin)
- Neuer Bereich **"E-Vergabe"** in den Firmeneinstellungen (`/admin/firma`)
- Sichtbar nur wenn E-Vergabe-Addon für die Firma freigeschaltet ist (durch Superadmin)
- Felder:
  - **Benutzer** (aus `_legacy/daten/conf.ini` → `[EVergabe] Benutzer`)
  - **Passwort** (aus `_legacy/daten/conf.ini` → `[EVergabe] Passwort`)
  - **Name** (aus `_legacy/daten/conf.ini` → `[EVergabe] Name`)

### 11.2 Superadmin – E-Vergabe Addon freigeben
- Superadmin kann E-Vergabe-Addon pro Firma freischalten/deaktivieren
- Dashboard: Spalte "E-Vergabe" mit Status und Toggle-Button (EV +/EV ✕)
- Firma-Detail: E-Vergabe-Bereich mit Status und Freischalten/Deaktivieren-Button

### 11.3 Berechtigungen (User-Ebene)
Drei neue Berechtigungen für Mitarbeiter:
- **E-Vergabe Addon nutzen** (`darf_evergabe_nutzen`) – Grundvoraussetzung für alle E-Vergabe-Funktionen
- **Kopfdaten holen erlauben** (`darf_kopfdaten_holen`) – Erlaubt das Abrufen von Kopfdaten aus der E-Vergabe
- **Aufmaße in E-Vergabe übertragen** (`darf_aufmass_uebertragen`) – Erlaubt das Übertragen von Aufmaßen

### 11.4 "Kopfdaten EV holen" Button
- Sichtbar nur wenn:
  - User hat `darf_evergabe_nutzen` UND `darf_kopfdaten_holen`
  - Firma hat `evergabe_aktiviert = True`
- Anklickbar nur wenn:
  - Firma hat `evergabe_benutzer` UND `evergabe_passwort` hinterlegt
- Mouseover-Hinweis wenn nicht anklickbar: *"Es sind keine E-Vergabe Logindaten hinterlegt."*
- Funktion: Ruft Kopfdaten von der E-Vergabe-API ab und füllt die Formularfelder (Bezeichnung, Bauabschnitt, SM-Nr, Abruf-Nr, Daten, Ansprechpartner)

### 11.5 Company-Modell (neue Felder)
- `evergabe_aktiviert` (Boolean, default=False) –Addon freigeschaltet
- `evergabe_benutzer` (String) – Login-Benutzername
- `evergabe_passwort` (String) – Login-Passwort
- `evergabe_name` (String) – Name/Kennung

### 11.6 E-Vergabe Service (`evergabe_service.py`)
Python-Port der AutoIt `EVvergabeWebobj.au3` Funktionen:
- **`EVergabeClient`** Klasse mit `requests.Session()` für Cookie-Handling
- **`login()`** – Login via POST `/public/login` mit CSRF-Token-Extraktion
- **`search_sm(sm_nr)`** – SM-Nr-Suche via GET `/framework-agreement-call`, extrahiert Title, Bedarfsnr, Belegnr, DetailsID, RV, Daten, Status
- **`get_aspa(details_id)`** – Ansprechpartner-Daten (Name, Email, Tel) aus Details-Seite
- **`hole_kopfdaten(sm_nr)`** – Kombiniert Login + Suche + ASPA → Rückgabe aller Kopfdaten
- **`aufmass_uebertragen(sm_nr, positionen, ...)`** – Überträgt Positionen in E-Vergabe:
  - Erstellt Sheet via `/sheet/create-sheet`
  - Für jede Position: Erstellt Position, dann INSERT je nach Einheit (ST, M, M2, M3, STD, LE)
  - Richtige Headers: User-Agent, Referer, Content-Type, CSRF-Token
  - URL-Encoding aller Parameter

### 11.7 Aufmaß in E-Vergabe übertragen
- Route: `POST /projekt/<id>/aufmass/<aufmass_id>/evergabe-uebertragen`
- Berechtigung: `darf_evergabe_nutzen` + `darf_aufmass_uebertragen`
- Überträgt alle Positionen des Aufmaßes in die E-Vergabe-Plattform
- Rückgabe: JSON mit Erfolg/Fehler pro Position
- `evergabe_sub` (String) – Sub-Kennung

### 11.8 User-Modell (neue Felder)
- `darf_evergabe_nutzen` (Boolean, default=False)
- `darf_kopfdaten_holen` (Boolean, default=False)
- `darf_aufmass_uebertragen` (Boolean, default=False)
