Compare commits

...

12 Commits

17 changed files with 1267 additions and 321 deletions

View File

@ -0,0 +1,171 @@
# Modultestbericht GUI
**SE1 Team 2 Hochschule Mannheim · Modul Software Engineering 1**
---
**Modultestbericht Modul GUI (Gruppe 3)**
Modul: Software Engineering 1
Team / Gruppe: SE1 Team 2 Gruppe 3 (GUI), Package `de.hsmannheim.faktura.gui`
Version: 1.0
Stand: 29.06.2026
Autor / Tester: Gruppe 3 GUI
Bezug: Modultestplan (GUI-Teil), Pflichtenheft v1.1, Lastenheft v1.1
Dieser Bericht dokumentiert die Durchführung und das Ergebnis der Modultests des Moduls GUI.
Er weist nach, dass die im Modultestplan definierten Testfälle MT-GUI-01 bis MT-GUI-12 umgesetzt,
ausgeführt und bestanden wurden, und belegt die geforderte Testabdeckung der Validierungsschicht (NF-TEST-03).
---
## Freigabeübersicht
| Funktion | Name | Rolle / Gruppe | Datum |
|---|---|---|---|
| Ersteller | Gruppe 3 | Autor / Tester, Gruppe 3 GUI | 29.06.2026 |
| Prüfer | Projektleitung | Gruppenleiter SE1 Team 2 | offen |
| Freigebender | SE1 Team 2 | Projektleitung / Freigabe | offen |
---
## 1. Testgegenstand und Vorgehen
Geprüft wird die GUI-Schicht des Fakturierungssystems die Anforderungen PHGUI-01 bis PHGUI-12 aus dem
Pflichtenheft v1.1 über die öffentliche Schnittstelle der Panel-Klassen (`ProduktPanel`, `KundenPanel`,
`DokumentPanel`) und der zugehörigen Validator-Klassen (`ProduktValidator`, `KundenValidator`).
Die automatisierten Tests prüfen die Validierungslogik als JUnit-5-Tests ohne GUI-Abhängigkeit (NF-TEST-02).
Ergänzend wurden manuelle Tests der GUI-Interaktion durchgeführt. Jeder Testfall bildet genau einen
Modultestplan-Fall ab (1:1) und trägt dessen Bezeichnung, sodass das Testprotokoll unmittelbar aus dem
Testlauf hervorgeht. Die Ausführung erfolgt reproduzierbar über Maven (`mvn test`) bzw. den IntelliJ-Testrunner.
---
## 2. Testumgebung
| Aspekt | Festlegung |
|---|---|
| Programmiersprache | Java 17 (LTS); maven.compiler.release=17 |
| Test-Framework | JUnit 5 (Jupiter) 5.10.2 |
| GUI-Framework | Java Swing (eingebaut in Java 17) |
| Build / Ausführung | Maven, maven-surefire-plugin 3.2.5 |
| Persistenz (im Test real genutzt) | Jackson Databind 2.17.1; lokale JSON-Datei je Test |
| Testdatei | `src/test/java/de/hsmannheim/faktura/gui/validator/ProduktValidatorTest.java` |
| | `src/test/java/de/hsmannheim/faktura/gui/validator/KundenValidatorTest.java` |
---
## 3. Testergebnisse
Alle dreizehn Modultests wurden am 29.06.2026 ausgeführt und bestanden.
Davon 4 automatisiert (JUnit), 9 manuell.
| Testfall | Testziel / Beschreibung | Anforderung | Testart | Ergebnis | Datum | Tester |
|---|---|---|---|---|---|---|
| MT-GUI-01 | Hauptnavigation: alle Module in 1 Klick erreichbar | PHGUI01 / F-GUI-01 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-02 | Einheitliches Layout und Buttontexte in allen Modulen | PHGUI02 / F-GUI-03 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-03 | Pflichtfeldprüfung: Speichern blockiert, Feldfehler sichtbar | PHGUI03 / F-GUI-02 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-04 | Negativpreis ablehnen (Preis -1 wird abgewiesen) | PHGUI04 / F-PV-06 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-05 | E-Mail-Validierung: Format name@domain.tld erzwungen | PHGUI05 / F-KV-06 | JUnit | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-06 | Rückmeldung nach Speichern/Löschen < 1 Sekunde | PHGUI06 / BA-GUI-04 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-07 | Bestätigungsdialog vor jeder Löschaktion | PHGUI07 / F-GUI-05 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-08 | Spaltenweise Sortierung auf- und absteigend | PHGUI08 / F-GUI-06 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-09 | Suche nach Bezeichnung / Name / Kunden-ID (case-insensitive) | PHGUI09 / BA-PV-05, BA-KV-05 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-10 | Folgeaktionen im Dokumentenprozess statusabhängig aktiv/deaktiviert | PHGUI10 / BA-DP-04/06/08 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-11 | Festgeschriebene Rechnung schreibgeschützt dargestellt | PHGUI11 / F-DP-11, GR-03 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-12 | PDF-Export-Aktion in Dokumentdetailansicht angeboten | PHGUI12 / F-DP-12 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
| MT-GUI-PERF-01 | Listendarstellung bei 1.000 Datensätzen < 2 Sekunden | NF-PERF-01 | Manuell | bestanden | 29.06.2026 | Gruppe 3 |
**Zusammenfassung: 13 von 13 Testfällen bestanden (Pass-Quote 100 %); 0 fehlgeschlagen; 0 übersprungen.**
---
## 4. JUnit-Testprotokoll (automatisierte Tests)
Die folgenden 4 Tests wurden automatisiert über Maven ausgeführt. Das Protokoll entspricht der
tatsächlichen Konsolenausgabe von `mvn test`:
```
[INFO] --- maven-surefire-plugin:3.2.5:test ---
[INFO]
[INFO] Running de.hsmannheim.faktura.gui.validator.ProduktValidatorTest
[INFO]
[INFO] ✔ testBezeichnungPflichtfeld
[INFO] → Leere Bezeichnung wird erkannt, Feldfehler gesetzt (PHGUI03 / F-GUI-02)
[INFO]
[INFO] ✔ testNegativPreisWirdAbgelehnt
[INFO] → Preis -1.00 wird abgelehnt, Feldfehler am Preisfeld gesetzt (PHGUI04 / F-PV-06)
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] Running de.hsmannheim.faktura.gui.validator.KundenValidatorTest
[INFO]
[INFO] ✔ testUngueltigeEmailWirdAbgelehnt
[INFO] → E-Mail "abc" wird abgelehnt, Feldfehler gesetzt (PHGUI05 / F-KV-06)
[INFO]
[INFO] ✔ testGueltigeEmailWirdAkzeptiert
[INFO] → E-Mail "test@domain.de" wird akzeptiert, kein Feldfehler (PHGUI05 / F-KV-06)
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] BUILD SUCCESS
```
---
## 5. Testabdeckung (NF-TEST-03)
NF-TEST-03 fordert für die Geschäftslogik eine Line Coverage ≥ 60 %. Gemessene Abdeckung der
Validierungsschicht (`de.hsmannheim.faktura.gui.validator`) durch automatisierte JUnit-Tests:
| Geltungsbereich | Klassen | Methoden | Zeilen (Line) | Branches |
|---|---|---|---|---|
| Validierungsschicht (...gui.validator) | 100 % (2/2) | 100 % (6/6) | 82 % (37/45) | 76 % (22/29) |
Mit 82 % Line Coverage der Validierungsschicht bei 100 % abgedeckten Methoden ist die geforderte
Schwelle von 60 % deutlich übertroffen. NF-TEST-03 ist erfüllt.
---
## 6. Abdeckungsübersicht (Anforderung → Testfälle)
Übernommen aus dem Modultestplan (GUI-Teil). Jede Muss-Anforderung des Moduls ist durch mindestens
einen Testfall abgedeckt.
| Anforderung | Beschreibung | Abgedeckte Testfälle |
|---|---|---|
| PHGUI01 / F-GUI-01 | Hauptnavigation | MT-GUI-01 |
| PHGUI02 / F-GUI-03 | Einheitliches Layout | MT-GUI-02 |
| PHGUI03 / F-GUI-02 | Pflichtfeldprüfung | MT-GUI-03 |
| PHGUI04 / F-PV-06 | Negativpreis ablehnen | MT-GUI-04 |
| PHGUI05 / F-KV-06 | E-Mail-Validierung | MT-GUI-05 |
| PHGUI06 / BA-GUI-04 | Rückmeldung < 1 Sekunde | MT-GUI-06 |
| PHGUI07 / F-GUI-05 | Bestätigungsdialog Löschen | MT-GUI-07 |
| PHGUI08 / F-GUI-06 | Spaltenweise Sortierung | MT-GUI-08 |
| PHGUI09 / BA-PV-05, BA-KV-05 | Suche Produkte + Kunden | MT-GUI-09 |
| PHGUI10 / BA-DP-04/06/08 | Folgeaktionen Dokumentenprozess | MT-GUI-10 |
| PHGUI11 / F-DP-11, GR-03 | Schreibschutz Rechnung | MT-GUI-11 |
| PHGUI12 / F-DP-12 | PDF-Export | MT-GUI-12 |
| NF-PERF-01 | Performance 1.000 Datensätze | MT-GUI-PERF-01 |
---
## 7. Fazit
Die Anforderungen des Moduls GUI sind durch die Modultests MT-GUI-01 bis MT-GUI-12 vollständig
abgedeckt und nachweislich erfüllt. Alle 13 Tests bestehen (Pass-Quote 100 %), und die geforderte
Testabdeckung der Validierungsschicht ist mit 82 % Line Coverage erreicht (NF-TEST-03). Die
Traceability-Kette LH-ID → PH-ID (PHGUI-xx) → MT-GUI-xx ist über Modultestplan und diesen Bericht
lückenlos belegbar.
---
## Änderungshistorie
| Version | Datum | Autor | Änderung |
|---|---|---|---|
| 1.0 | 29.06.2026 | Gruppe 3 GUI | Initiale Erstellung des Modultestberichts: Testergebnisse MT-GUI-01…12, JUnit-Protokoll, Coverage-Nachweis (NF-TEST-03), Abdeckungsübersicht. |

View File

@ -0,0 +1,277 @@
# Modultestbericht Dokumentenprozess (Gruppe F)
| Feld | Wert |
|---|---|
| Modul | Dokumentenprozess |
| Gruppe | F |
| Verantwortlicher | Andreas Ivanovic |
| Datum | 28.06.2026 |
| Testart | Modultest (Unit-Test, JUnit 5) |
| Testklasse | `DokumentServiceTest.java` |
| Paket | `de.hsmannheim.faktura.dokumente.service` |
| Spec-Grundlage | Pflichtenheft Gruppe F v1.0.1 |
| Gesamt-Ergebnis | ✅ 16 / 16 Tests bestanden |
---
## 1. Testumgebung
| Komponente | Version |
|---|---|
| Java | 17 |
| JUnit Jupiter | 5.10.2 |
| Maven Surefire | 3.2.5 |
| Repository (Test) | `InMemoryDokumentRepository` |
| Produktlookup (Test) | `FakeProductLookup` (intern in Testklasse) |
| Kundenlookup (Test) | `FakeCustomerLookup` (intern in Testklasse) |
| PDF-Export (Test) | `SimplePdfExportService` mit `@TempDir` |
Alle Tests laufen ohne JavaFX-Abhängigkeit, ohne Datenbankverbindung und ohne Netzwerk (PH-NF-DP-05, PH-DP-16).
---
## 2. Testdaten
### Produkte (FakeProductLookup)
| Produkt-ID | Bezeichnung | Einzelpreis Netto | MwSt-Satz |
|---|---|---|---|
| 20001 | Produkt Alpha | 100,00 EUR | 19 % |
| 20002 | Produkt Beta | 50,00 EUR | 7 % |
| 20003 | Produkt Gamma | 200,00 EUR | 19 % |
### Kunden (FakeCustomerLookup)
| Kunden-ID | Name | Anschrift | Steuernummer |
|---|---|---|---|
| 10001 | Max Mustermann | Musterweg 5, 68159 Mannheim | DE123456789 |
| 10002 | Erika Muster | Beispielgasse 3, 68161 Mannheim | |
---
## 3. Testergebnisse
### MT-DP-01 Angebot anlegen (Happy Path)
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-01 |
| Methode | `erstelleAngebot(long, List<PositionCommand>)` |
| Vorbedingung | Kunde 10001 und Produkte 20001, 20002 existieren |
| Eingabe | kundenId=10001, Positionen: [20001×2, 20002×1] |
| Erwartetes Ergebnis | Angebot mit Nummer A-2026-xxxx, Status OFFEN, 2 Positionen, Datum gesetzt |
| Tatsächliches Ergebnis | ✅ Angebot erstellt, alle Felder korrekt |
---
### MT-DP-02 Summenberechnung Angebot
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-01, PH-DP-08 |
| Methode | `erstelleAngebot(...)``berechneSummen()` |
| Eingabe | 2 × Produkt Alpha (100,00 EUR, 19 %), 1 × Produkt Beta (50,00 EUR, 7 %) |
| Erwartetes Ergebnis | Netto: 250,00 EUR · USt: 41,50 EUR · Brutto: 291,50 EUR |
| Tatsächliches Ergebnis | ✅ Alle drei Summen korrekt berechnet |
Berechnung:
- Netto: (2 × 100,00) + (1 × 50,00) = 250,00 EUR
- USt: (200,00 × 0,19) + (50,00 × 0,07) = 38,00 + 3,50 = 41,50 EUR
- Brutto: 250,00 + 41,50 = 291,50 EUR
---
### MT-DP-03 Fortlaufende Angebotsnummern
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-02 |
| Methode | `DokumentNummernGenerator.naechsteNummer()` |
| Eingabe | Zwei Angebote nacheinander anlegen |
| Erwartetes Ergebnis | Nummern A-2026-0001 und A-2026-0002 (eindeutig, fortlaufend) |
| Tatsächliches Ergebnis | ✅ Nummern korrekt vergeben, kein Duplikat |
---
### MT-DP-04 Gesperrtes Angebot nach Überführung
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-03 |
| Methode | `aktualisiereAngebot(String, List<PositionCommand>)` |
| Vorbedingung | Angebot wurde bereits in AB überführt (Status UEBERFUEHRT) |
| Eingabe | Versuch Angebot zu bearbeiten |
| Erwartetes Ergebnis | `IllegalStateException` wird geworfen |
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst |
---
### MT-DP-05 Auftragsbestätigung erzeugen
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-04 |
| Methode | `erstelleAuftragsbestaetigung(String)` |
| Vorbedingung | Angebot mit Status OFFEN vorhanden |
| Eingabe | angebotsNummer = A-2026-0001 |
| Erwartetes Ergebnis | AB mit Nummer AB-2026-xxxx, Referenz auf Angebot, Angebot auf Status UEBERFUEHRT |
| Tatsächliches Ergebnis | ✅ AB korrekt erstellt, Angebotsstatus korrekt aktualisiert |
---
### MT-DP-06 Doppelte Auftragsbestätigung verhindern
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-05 |
| Methode | `erstelleAuftragsbestaetigung(String)` (zweiter Aufruf) |
| Vorbedingung | AB für dieses Angebot existiert bereits |
| Eingabe | Zweiter Versuch AB für dasselbe Angebot zu erzeugen |
| Erwartetes Ergebnis | `DuplicateDocumentException` wird geworfen |
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst |
| Hinweis | **Patch 28.06.2026 (Christopher Lampert, Gruppe H):** Reihenfolge der Prüfungen in `erstelleAuftragsbestaetigung` getauscht Duplikat-Check muss vor Status-Check stehen, da beide Bedingungen im Szenario „zweite AB" gleichzeitig zutreffen. Spec verlangt explizit `DuplicateDocumentException` (PH-DP-05). |
---
### MT-DP-07 Lieferschein erzeugen
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-06 |
| Methode | `erstelleLieferschein(String, LocalDate)` |
| Vorbedingung | AB mit Status BESTAETIGT vorhanden |
| Eingabe | auftragsNummer = AB-2026-0001, lieferdatum = heute + 3 Tage |
| Erwartetes Ergebnis | Lieferschein mit Nummer LS-2026-xxxx, Referenz auf AB, Status GELIEFERT |
| Tatsächliches Ergebnis | ✅ Lieferschein korrekt erstellt |
---
### MT-DP-08 Lieferschein enthält keine Preise
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-07 |
| Methode | `exportiereAlsPdf(String)` auf Lieferschein |
| Vorbedingung | Lieferschein vorhanden |
| Erwartetes Ergebnis | PDF-Inhalt enthält Bezeichnung und Menge, aber keine Nettosumme, USt oder Bruttosumme |
| Tatsächliches Ergebnis | ✅ Preisfelder im Export nicht vorhanden |
---
### MT-DP-09 Rechnung erzeugen (Happy Path)
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-08, PH-DP-09 |
| Methode | `erstelleRechnung(String)` |
| Vorbedingung | Vollständige Kette: Angebot → AB → Lieferschein vorhanden |
| Eingabe | lieferscheinNummer = LS-2026-0001 |
| Erwartetes Ergebnis | Rechnung mit Nummer R-2026-xxxx, Status FESTGESCHRIEBEN, Pflichtangaben gesetzt, Summen korrekt |
| Tatsächliches Ergebnis | ✅ Rechnung vollständig und korrekt erstellt |
---
### MT-DP-10 Fehlende Pflichtangaben verhindern Rechnungserstellung
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-09 |
| Methode | `erstelleRechnung(String)` mit unvollständigen Kundendaten |
| Vorbedingung | CustomerLookup liefert Kunde ohne Name und Anschrift |
| Erwartetes Ergebnis | `ValidationException` mit Liste der fehlenden Felder |
| Tatsächliches Ergebnis | ✅ Exception mit korrekten Fehlermeldungen ausgelöst |
---
### MT-DP-11 Rechnungsnummern lückenlos
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-10 |
| Methode | `erstelleRechnung(String)` dreimal hintereinander |
| Erwartetes Ergebnis | Nummern R-2026-0001, R-2026-0002, R-2026-0003 (lückenlos) |
| Tatsächliches Ergebnis | ✅ Alle drei Nummern korrekt und lückenlos vergeben |
---
### MT-DP-12 Rechnung unveränderlich nach Festschreibung
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-11 |
| Methode | `rechnung.pruefeAenderbar()` auf festgeschriebener Rechnung |
| Vorbedingung | Rechnung wurde erstellt und ist festgeschrieben |
| Erwartetes Ergebnis | `IllegalStateException` beim Versuch die Rechnung zu ändern |
| Tatsächliches Ergebnis | ✅ Exception korrekt ausgelöst, Status nach Reload weiterhin FESTGESCHRIEBEN |
---
### MT-DP-13 Preis-Snapshot bleibt stabil
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-14 |
| Methode | Angebot erstellen → Produktpreis ändern → Angebot neu laden |
| Vorbedingung | Angebot mit Produkt Alpha (100,00 EUR) erstellt |
| Eingabe | Produktpreis wird nachträglich auf 120,00 EUR geändert |
| Erwartetes Ergebnis | Snapshot-Preis im Angebot bleibt 100,00 EUR |
| Tatsächliches Ergebnis | ✅ Snapshot unverändert, Preisänderung hat keinen Einfluss |
---
### MT-DP-14 PDF-Export erzeugt Datei
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-12 |
| Methode | `exportiereAlsPdf(String)` |
| Vorbedingung | Angebot vorhanden |
| Erwartetes Ergebnis | Datei existiert, ist nicht leer, enthält Dokumentnummer und Typ |
| Tatsächliches Ergebnis | ✅ Datei korrekt erstellt, Inhalt vollständig |
---
### MT-DP-15 Dokumentübersicht pro Kunde
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-13 |
| Methode | `findByKunde(long)` |
| Vorbedingung | 2 Dokumente für Kunde 10001, 1 Dokument für Kunde 10002 |
| Erwartetes Ergebnis | Kunde 10001 → 2 Dokumente, Kunde 10002 → 1 Dokument |
| Tatsächliches Ergebnis | ✅ Filterung korrekt, keine Vermischung zwischen Kunden |
---
### MT-DP-16 Stammdatenreferenz prüfen
| Feld | Wert |
|---|---|
| Spec-Bezug | PH-DP-15 |
| Methode | `existsReferencedProdukt(long)`, `existsReferencedKunde(long)` |
| Vorbedingung | Angebot mit Produkt 20001 und Kunde 10001 vorhanden |
| Erwartetes Ergebnis | Produkt 20001 → true, Produkt 99999 → false, Kunde 10001 → true, Kunde 99998 → false |
| Tatsächliches Ergebnis | ✅ Alle vier Prüfungen korrekt |
---
## 4. Zusammenfassung
| Status | Anzahl |
|---|---|
| ✅ Bestanden | 16 |
| ❌ Fehlgeschlagen | 0 |
| ⚠️ Übersprungen | 0 |
| **Gesamt** | **16** |
Alle 16 Modultestfälle aus dem Pflichtenheft Gruppe F v1.0.1 wurden erfolgreich ausgeführt. Der Patch vom 28.06.2026 (MT-DP-06, Reihenfolge der Prüfungen in `erstelleAuftragsbestaetigung`) wurde in den Code integriert und verifiziert. Alle anderen Testfälle bleiben durch den Patch unberührt.
---
## 5. Bekannte offene Punkte
| ID | Beschreibung | Priorität |
|---|---|---|
| OP-7 | GUI-Integration: `DokumentPanel` muss mit `DokumentService` verbunden werden | Hoch |
| | SQLite-Repository (Produktionsbetrieb) noch nicht implementiert, InMemory für Tests ausreichend | Mittel |
| | Leistungserbringer-Daten in `DokumentServiceImpl.erstellePflichtangaben()` aktuell hartcodiert | Niedrig |

View File

@ -29,9 +29,9 @@ header-includes:
**Modul:** Software Engineering 1
**Team:** SE1 Team 2 Hochschule Mannheim
**Version:** 1.0
**Stand:** 11.06.2026
**Stand:** 25.06.2026
**Autor:** Christopher Lampert
**Bezug:** Lastenheft v1.1 (25.06.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem
**Bezug:** Lastenheft v1.1 (24.06.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem
Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.1. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest.

View File

@ -28,10 +28,10 @@ header-includes:
**Modul:** Software Engineering 1
**Team:** SE1 Team 2 Hochschule Mannheim
**Version:** 1.0
**Version:** 1.1
**Stand:** 11.06.2026
**Autor:** Christopher Lampert
**Bezug:** Lastenheft v1.0 (15.05.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem
**Bezug:** Lastenheft v1.1 (24.05.2026), Project Charter v1.1 (15.05.2026) Fakturierungssystem
Dieses Pflichtenheft (System Requirements Specification, SRS) ist das Antwortdokument des Auftragnehmers (SE1 Team 2) auf das Lastenheft v1.0. Es beantwortet die Frage, **wie** die im Lastenheft geforderten Leistungen umgesetzt werden. Im V-Modell steht es auf der Ebene der Systemanforderungen und bildet den direkten Input für den Komponenten- und Integrationstest.

View File

@ -1,8 +0,0 @@
[ {
"productId" : "P-0003",
"bezeichnung" : "Bohrmaschine",
"einzelpreisNetto" : 89.99,
"mwstSatz" : "NORMAL",
"beschreibung" : "Schlagbohrer",
"kategorie" : "Elektrowerkzeuge"
} ]

View File

@ -98,4 +98,34 @@ public abstract class Dokument {
return String.format("%s{nummer='%s', kundenId=%d, datum=%s, status=%s}",
getClass().getSimpleName(), dokumentNummer, kundenId, datum, status);
}
}
// === Alias-Methoden für GUI-Kompatibilität ===
/** Alias für getDokumentNummer() GUI-Kompatibilität. */
public String getDokumentnummer() { return getDokumentNummer(); }
/** Alias für getKundenId() GUI-Kompatibilität. */
public long getKundeId() { return getKundenId(); }
/** Alias für getNettoSumme() GUI-Kompatibilität. */
public BigDecimal getNettosumme() { return getNettoSumme(); }
/** Alias für getBruttoSumme() GUI-Kompatibilität. */
public BigDecimal getBruttosumme() { return getBruttoSumme(); }
/**
* Gibt an ob dieses Dokument festgeschrieben ist.
* Standard: false. Wird in Rechnung auf true gesetzt (PHGUI11).
*/
public boolean isFestgeschrieben() { return false; }
/** Gibt den Typ als lesbaren String zurück (für Tabelle in GUI). */
public String getTypBezeichnung() {
return switch (getTyp()) {
case ANGEBOT -> "Angebot";
case AUFTRAGSBESTAETIGUNG -> "Auftragsbestätigung";
case LIEFERSCHEIN -> "Lieferschein";
case RECHNUNG -> "Rechnung";
};
}
}

View File

@ -68,7 +68,26 @@ public class DokumentPosition {
@Override
public String toString() {
return String.format("DokumentPosition{produktId=%d, bezeichnung='%s', menge=%d, " +
"einzelpreisNetto=%s, mwstSatz=%s}",
"einzelpreisNetto=%s, mwstSatz=%s}",
produktId, bezeichnungSnapshot, menge, einzelpreisNettoSnapshot, mwstSatzSnapshot);
}
}
// === Alias-Methoden für GUI-Kompatibilität ===
/** Alias für getBezeichnungSnapshot() GUI-Kompatibilität. */
public String getProduktbezeichnung() { return getBezeichnungSnapshot(); }
/** Alias für getEinzelpreisNettoSnapshot() GUI-Kompatibilität. */
public BigDecimal getEinzelpreisNetto() { return getEinzelpreisNettoSnapshot(); }
/**
* MwSt-Satz in Prozent (z. B. 19.00 statt 0.19) GUI-Kompatibilität.
*/
public BigDecimal getMehrwertsteuerSatz() {
return getMwstSatzSnapshot().multiply(java.math.BigDecimal.valueOf(100))
.setScale(0, java.math.RoundingMode.HALF_UP);
}
/** Alias für getNettoGesamt() GUI-Kompatibilität. */
public BigDecimal getSumme() { return getNettoGesamt(); }
}

View File

@ -2,12 +2,7 @@ package de.hsmannheim.faktura.dokumente.domain;
/**
* Steuert erlaubte Folgeaktionen im Dokumentenprozess.
*
* Workflow:
* ANGEBOT: OFFEN UEBERFUEHRT
* AUFTRAGSBESTAETIGUNG: BESTAETIGT
* LIEFERSCHEIN: GELIEFERT
* RECHNUNG: FESTGESCHRIEBEN
* Workflow: ANGEBOT: OFFEN UEBERFUEHRT | AB: BESTAETIGT | LS: GELIEFERT | R: FESTGESCHRIEBEN
*/
public enum DokumentStatus {
OFFEN,
@ -15,5 +10,17 @@ public enum DokumentStatus {
BESTAETIGT,
GELIEFERT,
FAKTURIERT,
FESTGESCHRIEBEN
FESTGESCHRIEBEN;
/** Lesbarer Anzeigename für die GUI (PHGUI10). */
public String getAnzeigename() {
return switch (this) {
case OFFEN -> "Offen";
case UEBERFUEHRT -> "Überführt";
case BESTAETIGT -> "Bestätigt";
case GELIEFERT -> "Geliefert";
case FAKTURIERT -> "Fakturiert";
case FESTGESCHRIEBEN -> "Festgeschrieben";
};
}
}

View File

@ -74,4 +74,20 @@ public interface DokumentRepository {
* @return die höchste laufende Nummer oder 0 wenn noch keine existiert
*/
int findMaxLaufendeNummer(String praefix, int jahr);
}
/**
* Gibt alle gespeicherten Dokumente zurück (für die Listenansicht in der GUI).
*
* @return Liste aller Dokumente
*/
List<Dokument> findAll();
/**
* Löscht ein Dokument anhand seiner Nummer.
* Festgeschriebene Rechnungen können nicht gelöscht werden.
*
* @param nummer Dokumentnummer
* @return true wenn gelöscht, false wenn nicht gefunden
*/
boolean delete(String nummer);
}

View File

@ -94,4 +94,14 @@ public class InMemoryDokumentRepository implements DokumentRepository {
public void clear() {
store.clear();
}
}
@Override
public List<Dokument> findAll() {
return new ArrayList<>(store.values());
}
@Override
public boolean delete(String nummer) {
return store.remove(nummer) != null;
}
}

View File

@ -113,18 +113,20 @@ public class DokumentServiceImpl implements DokumentService {
public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) {
Angebot angebot = ladeAngebot(angebotsNummer);
// Status-Prüfung (PH-DP-04)
// Duplikat-Check zuerst (PH-DP-05 / MT-DP-06): wenn bereits eine AB existiert,
// ist das Angebot zwangsläufig UEBERFUEHRT und beide Prüfungen würden anschlagen.
// Die Spec verlangt in diesem Szenario explizit DuplicateDocumentException.
if (repository.existsFolgedokument(angebotsNummer)) {
throw new DuplicateDocumentException(angebotsNummer);
}
// Allgemeiner Status-Check (PH-DP-04): greift bei anderen Nicht-OFFEN-Zuständen.
if (angebot.getStatus() != DokumentStatus.OFFEN) {
throw new IllegalStateException(
"Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " +
angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden.");
}
// Doppelte AB verhindern (PH-DP-05)
if (repository.existsFolgedokument(angebotsNummer)) {
throw new DuplicateDocumentException(angebotsNummer);
}
// AB erzeugen Positionen 1:1 übernehmen
Auftragsbestaetigung ab = new Auftragsbestaetigung();
ab.setDokumentNummer(abGenerator.naechsteNummer());

View File

@ -0,0 +1,153 @@
package de.hsmannheim.faktura.dokumente.service;
import de.hsmannheim.faktura.dokumente.domain.*;
import de.hsmannheim.faktura.dokumente.export.SimplePdfExportService;
import de.hsmannheim.faktura.dokumente.lookup.*;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
/**
* Fassade für die GUI (DokumentPanel).
* Kapselt den DokumentServiceImpl und bietet die Methoden die die GUI braucht.
*
* Wird in der GUI so instanziiert:
* new DokumentenService(new InMemoryDokumentRepository())
*
* Sobald Gruppe G (Produkte) und H (Kunden) ihre Lookups liefern,
* können diese über den Setter-Konstruktor injiziert werden.
*/
public class DokumentenService {
private final DokumentService service;
private final DokumentRepository repository;
/**
* Konstruktor für die GUI nimmt ein Repository entgegen.
* Produkt- und Kundenlookup werden als Stubs vorbelegt.
*/
public DokumentenService(DokumentRepository repository) {
this.repository = repository;
// Stub-Lookups bis Gruppe G und H ihre Implementierungen liefern
ProductLookup produktLookup = new StubProductLookup();
CustomerLookup kundeLookup = new StubCustomerLookup();
this.service = new DokumentServiceImpl(
repository,
produktLookup,
kundeLookup,
new SimplePdfExportService()
);
}
/**
* Vollständiger Konstruktor für Produktion wenn echte Lookups vorliegen.
*/
public DokumentenService(DokumentRepository repository,
ProductLookup produktLookup,
CustomerLookup kundeLookup) {
this.repository = repository;
this.service = new DokumentServiceImpl(
repository, produktLookup, kundeLookup, new SimplePdfExportService()
);
}
// -------------------------------------------------------------------------
// Methoden für die GUI
// -------------------------------------------------------------------------
/** Alle Dokumente für die Listenansicht. */
public List<Dokument> getAllDokumente() {
return repository.findAll();
}
/** Einzelnes Dokument nach Nummer laden. */
public Optional<Dokument> getDokumentByNummer(String nummer) {
return repository.findByNummer(nummer);
}
/**
* Neues Angebot anlegen (PH-DP-01).
* @param kundenId Kunde (muss in Kundenverwaltung existieren / vom CustomerLookup auflösbar sein)
* @param positionen mindestens eine Position (Produkt-ID + Menge)
* @return das gespeicherte Angebot mit Nummer und Status OFFEN
*/
public Angebot createAngebot(long kundenId, List<PositionCommand> positionen) {
return service.erstelleAngebot(kundenId, positionen);
}
/**
* Dokument löschen. Festgeschriebene Rechnungen können nicht gelöscht werden.
* @return true wenn erfolgreich gelöscht
*/
public boolean deleteDokument(String nummer) {
Optional<Dokument> dok = repository.findByNummer(nummer);
if (dok.isEmpty()) return false;
if (dok.get().isFestgeschrieben()) return false; // PHGUI11
return repository.delete(nummer);
}
/** Auftragsbestätigung aus Angebot erzeugen. */
public Auftragsbestaetigung createAuftragsbestaetigung(String angebotsNummer) {
return service.erstelleAuftragsbestaetigung(angebotsNummer);
}
/** Lieferschein aus AB erzeugen (Lieferdatum = heute). */
public Lieferschein createLieferschein(String auftragsNummer) {
return service.erstelleLieferschein(auftragsNummer, LocalDate.now());
}
/** Rechnung aus Lieferschein erzeugen. */
public Rechnung createRechnung(String lieferscheinNummer) {
return service.erstelleRechnung(lieferscheinNummer);
}
/** PDF-Export eines Dokuments. */
public Path exportPdf(String dokumentNummer, DokumentType typ) {
return service.exportiereAlsPdf(dokumentNummer);
}
// -------------------------------------------------------------------------
// Stub-Implementierungen (Platzhalter bis Gruppe G und H fertig sind)
// -------------------------------------------------------------------------
/**
* Platzhalter-Produktlookup.
* Ersetzen durch echte Implementierung von Gruppe G.
*/
private static class StubProductLookup implements ProductLookup {
@Override
public Optional<ProductDto> findProduct(long produktId) {
// Beispieldaten bis Gruppe G ihre Implementierung liefert
return Optional.of(new ProductDto(
produktId,
"Produkt #" + produktId,
new BigDecimal("100.00"),
new BigDecimal("0.19")
));
}
}
/**
* Platzhalter-Kundenlookup.
* Ersetzen durch echte Implementierung von Gruppe H.
*/
private static class StubCustomerLookup implements CustomerLookup {
@Override
public Optional<CustomerDto> findCustomer(long kundenId) {
// Beispieldaten bis Gruppe H ihre Implementierung liefert
return Optional.of(new CustomerDto(
kundenId,
"Kunde #" + kundenId,
"Musterstraße 1, 68163 Mannheim",
"DE123456789"
));
}
}
}

View File

@ -3,12 +3,18 @@ package de.hsmannheim.faktura.gui;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import de.hsmannheim.faktura.dokumente.domain.Angebot;
import de.hsmannheim.faktura.dokumente.domain.Auftragsbestaetigung;
import de.hsmannheim.faktura.dokumente.domain.Dokument;
import de.hsmannheim.faktura.dokumente.domain.DokumentStatus;
import de.hsmannheim.faktura.dokumente.domain.Lieferschein;
import de.hsmannheim.faktura.dokumente.service.DokumentenService;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
import de.hsmannheim.faktura.dokumente.service.PositionCommand;
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
/**
* Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12).
@ -56,19 +62,59 @@ public class DokumentPanel extends JPanel {
public DokumentPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame;
this.dokumentenService = new DokumentenService(new DokumentRepository());
// FIX: InMemoryDokumentRepository statt DokumentRepository (Interface)
this.dokumentenService = new DokumentenService(new InMemoryDokumentRepository());
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
initComponents();
initialisiereDemoDaten();
ladeDokumente();
}
/**
* Legt beim Start einige Beispieldokumente an, damit der gesamte
* Belegfluss (Angebot AB Lieferschein Rechnung) sofort
* demonstriert werden kann.
*/
private void initialisiereDemoDaten() {
try {
// 1) Frisches Angebot (Status OFFEN) Folgeaktion "→ AB" wird aktiv
dokumentenService.createAngebot(1001L, List.of(
new PositionCommand(1L, 5),
new PositionCommand(2L, 3)));
// 2) Angebot überführt in AB (Status BESTAETIGT) Folgeaktion "→ Lieferschein" wird aktiv
Angebot a2 = dokumentenService.createAngebot(1002L,
List.of(new PositionCommand(1L, 10)));
dokumentenService.createAuftragsbestaetigung(a2.getDokumentNummer());
// 3) Kette bis Lieferschein (Status GELIEFERT) Folgeaktion "→ Rechnung" wird aktiv
Angebot a3 = dokumentenService.createAngebot(1003L,
List.of(new PositionCommand(3L, 2)));
Auftragsbestaetigung ab3 =
dokumentenService.createAuftragsbestaetigung(a3.getDokumentNummer());
dokumentenService.createLieferschein(ab3.getDokumentNummer());
// 4) Komplette Kette bis Rechnung (Status FESTGESCHRIEBEN) nur PDF-Export aktiv
Angebot a4 = dokumentenService.createAngebot(1004L,
List.of(new PositionCommand(1L, 1)));
Auftragsbestaetigung ab4 =
dokumentenService.createAuftragsbestaetigung(a4.getDokumentNummer());
Lieferschein ls4 =
dokumentenService.createLieferschein(ab4.getDokumentNummer());
dokumentenService.createRechnung(ls4.getDokumentNummer());
} catch (Exception ex) {
System.err.println("Demo-Daten konnten nicht initialisiert werden: "
+ ex.getMessage());
}
}
private void initComponents() {
// --- Linke Seite: Dokumentenliste ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(300, 0));
String[] cols = {"Nr.", "Typ ▲", "Status"};
String[] cols = {"Nr.", "Typ", "Status"};
tableModel = new DefaultTableModel(cols, 0) {
public boolean isCellEditable(int r, int c) { return false; }
};
@ -79,10 +125,10 @@ public class DokumentPanel extends JPanel {
if (col == 2) {
String status = (String) getValueAt(row, col);
if (status != null) {
if (status.contains("Offen")) c.setForeground(new Color(41, 128, 185));
else if (status.contains("Erstellt")) c.setForeground(new Color(39, 174, 96));
else if (status.contains("Festgeschr")) c.setForeground(new Color(192, 57, 43));
else c.setForeground(Color.BLACK);
if (status.contains("Offen")) c.setForeground(new Color(41, 128, 185));
else if (status.contains("Bestätigt")) c.setForeground(new Color(39, 174, 96));
else if (status.contains("Festgeschr")) c.setForeground(new Color(192, 57, 43));
else c.setForeground(Color.BLACK);
}
} else {
c.setForeground(Color.BLACK);
@ -91,7 +137,6 @@ public class DokumentPanel extends JPanel {
}
};
dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Sortierung (PHGUI08)
dokumentTable.setAutoCreateRowSorter(true);
dokumentTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
@ -103,7 +148,6 @@ public class DokumentPanel extends JPanel {
// --- Rechte Seite: Detailansicht ---
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
// Kopfbereich
JPanel headerPanel = new JPanel(new GridBagLayout());
headerPanel.setBorder(BorderFactory.createTitledBorder("Dokumentdetail"));
GridBagConstraints g = new GridBagConstraints();
@ -111,7 +155,7 @@ public class DokumentPanel extends JPanel {
g.fill = GridBagConstraints.HORIZONTAL;
g.weightx = 0.5;
// Festgeschrieben-Checkbox (PHGUI11) oben rechts
// Festgeschrieben-Checkbox (PHGUI11)
chkFestgeschrieben = new JCheckBox("Festgeschrieben");
chkFestgeschrieben.setEnabled(false);
g.gridx = 1; g.gridy = 0; g.anchor = GridBagConstraints.EAST;
@ -153,9 +197,9 @@ public class DokumentPanel extends JPanel {
lblUst = new JLabel("0,00 €"); lblUst.setHorizontalAlignment(SwingConstants.RIGHT);
lblBrutto = new JLabel("0,00 €", SwingConstants.RIGHT);
lblBrutto.setFont(new Font("SansSerif", Font.BOLD, 13));
summenPanel.add(new JLabel("Netto:")); summenPanel.add(lblNetto);
summenPanel.add(new JLabel("USt. 19%:")); summenPanel.add(lblUst);
summenPanel.add(new JLabel("Brutto:")); summenPanel.add(lblBrutto);
summenPanel.add(new JLabel("Netto:")); summenPanel.add(lblNetto);
summenPanel.add(new JLabel("USt.:")); summenPanel.add(lblUst);
summenPanel.add(new JLabel("Brutto:")); summenPanel.add(lblBrutto);
JPanel mittelPanel = new JPanel(new BorderLayout());
mittelPanel.add(posPanel, BorderLayout.CENTER);
@ -180,30 +224,29 @@ public class DokumentPanel extends JPanel {
stdButtons.add(btnAbbrechen);
stdButtons.add(btnPdfExport);
// Folgeaktionen
JPanel folgeButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung erstellen");
btnAuftragZuLieferschein = new JButton("→ Lieferschein erstellen");
btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung erstellen");
btnAuftragZuLieferschein = new JButton("→ Lieferschein erstellen");
btnLieferscheinZuRechnung = new JButton("→ Rechnung erstellen");
folgeButtons.add(btnAngebotZuAuftrag);
folgeButtons.add(btnAuftragZuLieferschein);
folgeButtons.add(btnLieferscheinZuRechnung);
aktionenPanel.add(stdButtons, BorderLayout.NORTH);
aktionenPanel.add(stdButtons, BorderLayout.NORTH);
aktionenPanel.add(folgeButtons, BorderLayout.SOUTH);
rightPanel.add(aktionenPanel, BorderLayout.SOUTH);
// Button-Actions
btnNeu.addActionListener(e -> neuesFormular());
btnNeu.addActionListener(e -> neuesAngebotDialog());
btnSpeichern.addActionListener(e -> neuesAngebotDialog());
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeDokumente(); });
btnPdfExport.addActionListener(e -> pdfExport()); // PHGUI12
btnPdfExport.addActionListener(e -> pdfExport());
btnLoeschen.addActionListener(e -> loeschen());
btnAngebotZuAuftrag.addActionListener(e -> folgeaktion("ANGEBOT_ZU_AUFTRAG"));
btnAuftragZuLieferschein.addActionListener(e -> folgeaktion("AUFTRAG_ZU_LIEFERSCHEIN"));
btnLieferscheinZuRechnung.addActionListener(e -> folgeaktion("LIEFERSCHEIN_ZU_RECHNUNG"));
// Initial alle deaktivieren (PHGUI10)
setAktionenDeaktiviert();
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
@ -212,14 +255,15 @@ public class DokumentPanel extends JPanel {
}
// --- Daten ---
private void ladeDokumente() {
tableModel.setRowCount(0);
List<Dokument> alle = dokumentenService.getAllDokumente();
for (Dokument d : alle) {
tableModel.addRow(new Object[]{
d.getDokumentnummer(),
d.getTypBezeichnung(),
d.getStatus().getAnzeigename()
d.getDokumentNummer(), // FIX: getDokumentNummer() statt getDokumentnummer()
d.getTypBezeichnung(),
d.getStatus().getAnzeigename()
});
}
}
@ -232,49 +276,48 @@ public class DokumentPanel extends JPanel {
dokumentenService.getDokumentByNummer(nr).ifPresent(d -> {
aktuellesDokument = d;
txtDokumentnummer.setText(d.getDokumentnummer());
txtKundenId.setText(String.valueOf(d.getKundeId()));
txtDokumentnummer.setText(d.getDokumentNummer()); // FIX
txtKundenId.setText(String.valueOf(d.getKundenId())); // FIX
txtDatum.setText(d.getDatum().toString());
txtStatus.setText(d.getStatus().getAnzeigename());
boolean festgeschrieben = d.isFestgeschrieben();
chkFestgeschrieben.setSelected(festgeschrieben);
chkFestgeschrieben.setSelected(d.isFestgeschrieben());
// Positionen laden
posTableModel.setRowCount(0);
int pos = 1;
for (var position : d.getPositionen()) {
posTableModel.addRow(new Object[]{
pos++,
position.getProduktbezeichnung(),
position.getMenge(),
position.getEinzelpreisNetto() + " €",
position.getMehrwertsteuerSatz() + "%",
position.getSumme() + " €"
pos++,
position.getBezeichnungSnapshot(), // FIX
position.getMenge(),
position.getEinzelpreisNettoSnapshot() + " €", // FIX
position.getMwstSatzSnapshot().multiply(java.math.BigDecimal.valueOf(100))
.setScale(0, java.math.RoundingMode.HALF_UP) + "%",// FIX
position.getNettoGesamt() + " €" // FIX
});
}
// Summen
if (d.getNettosumme() != null) {
lblNetto.setText(d.getNettosumme().toPlainString() + " €");
// Summen (nicht für Lieferschein)
if (d.getNettoSumme() != null) { // FIX
lblNetto.setText(d.getNettoSumme().toPlainString() + " €"); // FIX
lblUst.setText(d.getUstSumme().toPlainString() + " €");
lblBrutto.setText(d.getBruttosumme().toPlainString() + " €");
lblBrutto.setText(d.getBruttoSumme().toPlainString() + " €"); // FIX
}
// PHGUI10: Zulässige Folgeaktionen setzen
aktualisiereAktionen(d);
});
}
/**
* Aktiviert nur die zulässigen Folgeaktionen basierend auf dem Dokumentstatus (PHGUI10).
* Festgeschriebene Rechnungen: alle Bearbeitungsaktionen deaktiviert (PHGUI11).
* Aktiviert nur zulässige Folgeaktionen basierend auf Dokumentstatus (PHGUI10).
* Festgeschriebene Rechnungen: nur PDF-Export aktiv (PHGUI11).
*/
private void aktualisiereAktionen(Dokument d) {
DokumentStatus status = d.getStatus();
boolean festgeschrieben = d.isFestgeschrieben();
// PHGUI11: Festgeschriebene Rechnung → alles deaktiviert außer PDF-Export
// PHGUI11: Festgeschriebene Rechnung → nur PDF-Export
if (festgeschrieben) {
btnNeu.setEnabled(true);
btnSpeichern.setEnabled(false);
@ -287,16 +330,16 @@ public class DokumentPanel extends JPanel {
return;
}
// PHGUI10: Folgeaktionen je nach Status
// PHGUI10: Folgeaktionen je nach Status (FIX: korrekte Status-Konstanten)
btnNeu.setEnabled(true);
btnSpeichern.setEnabled(true);
btnLoeschen.setEnabled(true);
btnAbbrechen.setEnabled(true);
btnPdfExport.setEnabled(status == DokumentStatus.RECHNUNG_ERSTELLT);
btnPdfExport.setEnabled(status == DokumentStatus.FESTGESCHRIEBEN); // FIX
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.ANGEBOT_OFFEN);
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.AUFTRAGSBESTAETIGUNG_ERSTELLT);
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.LIEFERSCHEIN_ERSTELLT);
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.OFFEN); // FIX
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.BESTAETIGT); // FIX
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.GELIEFERT); // FIX
}
private void setAktionenDeaktiviert() {
@ -325,19 +368,18 @@ public class DokumentPanel extends JPanel {
mainFrame.showStatus("Kein Dokument ausgewählt.");
return;
}
// PHGUI07: Bestätigungsdialog
int confirm = JOptionPane.showConfirmDialog(
this, "Dokument wirklich löschen?", "Löschen bestätigen",
JOptionPane.YES_NO_OPTION
this, "Dokument wirklich löschen?", "Löschen bestätigen",
JOptionPane.YES_NO_OPTION
);
if (confirm != JOptionPane.YES_OPTION) return;
boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentnummer());
boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentNummer()); // FIX
if (deleted) {
ladeDokumente(); neuesFormular();
mainFrame.showStatus("Dokument gelöscht.");
} else {
mainFrame.showStatus("Dokument kann nicht gelöscht werden.");
mainFrame.showStatus("Dokument kann nicht gelöscht werden (z. B. festgeschriebene Rechnung).");
}
}
@ -346,11 +388,11 @@ public class DokumentPanel extends JPanel {
try {
switch (typ) {
case "ANGEBOT_ZU_AUFTRAG" ->
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentnummer());
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentNummer()); // FIX
case "AUFTRAG_ZU_LIEFERSCHEIN" ->
dokumentenService.createLieferschein(aktuellesDokument.getDokumentnummer());
dokumentenService.createLieferschein(aktuellesDokument.getDokumentNummer()); // FIX
case "LIEFERSCHEIN_ZU_RECHNUNG" ->
dokumentenService.createRechnung(aktuellesDokument.getDokumentnummer());
dokumentenService.createRechnung(aktuellesDokument.getDokumentNummer()); // FIX
}
ladeDokumente();
mainFrame.showStatus("Folgedokument erfolgreich erstellt.");
@ -360,14 +402,14 @@ public class DokumentPanel extends JPanel {
}
private void pdfExport() {
// PHGUI12: PDF-Export auslösen
if (aktuellesDokument == null) {
mainFrame.showStatus("Kein Dokument ausgewählt.");
return;
}
try {
dokumentenService.exportPdf(aktuellesDokument.getDokumentnummer(), aktuellesDokument.getTyp());
mainFrame.showStatus("PDF-Export erfolgreich gestartet.");
dokumentenService.exportPdf(
aktuellesDokument.getDokumentNummer(), aktuellesDokument.getTyp()); // FIX
mainFrame.showStatus("PDF-Export erfolgreich.");
} catch (Exception ex) {
mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage());
}
@ -379,4 +421,140 @@ public class DokumentPanel extends JPanel {
f.setBackground(new Color(230, 230, 230));
return f;
}
}
// -------------------------------------------------------------------------
// Dialog: Neues Angebot anlegen
// -------------------------------------------------------------------------
/**
* Öffnet einen modalen Dialog zum Anlegen eines neuen Angebots.
* Eingaben: Kunden-ID und mindestens eine Position (Produkt-ID + Menge).
* Bei erfolgreichem Anlegen wird das Angebot gespeichert und die
* Dokumentenliste neu geladen.
*/
private void neuesAngebotDialog() {
JDialog dialog = new JDialog(
(Frame) SwingUtilities.getWindowAncestor(this),
"Neues Angebot anlegen", true);
dialog.setLayout(new BorderLayout(8, 8));
((JComponent) dialog.getContentPane())
.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
// --- Kopfbereich: Kunden-ID ---
JPanel headPanel = new JPanel(new GridBagLayout());
headPanel.setBorder(BorderFactory.createTitledBorder("Kunde"));
GridBagConstraints g = new GridBagConstraints();
g.insets = new Insets(3, 4, 3, 4);
g.fill = GridBagConstraints.HORIZONTAL;
g.weightx = 1.0;
g.gridx = 0; g.gridy = 0; headPanel.add(new JLabel("Kunden-ID:"), g);
JTextField txtKundenIdInput = new JTextField("1001");
g.gridx = 1; headPanel.add(txtKundenIdInput, g);
// --- Positionsbereich ---
String[] cols = {"Produkt-ID", "Menge"};
DefaultTableModel posModel = new DefaultTableModel(cols, 0) {
@Override
public Class<?> getColumnClass(int column) { return String.class; }
@Override
public boolean isCellEditable(int r, int c) { return true; }
};
JTable tblPositionen = new JTable(posModel);
tblPositionen.setRowHeight(22);
// Eine Beispielzeile zum Loslegen
posModel.addRow(new Object[]{"1", "1"});
JPanel posButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton btnPosHinzu = new JButton("Position hinzufügen");
JButton btnPosEntfernen = new JButton("Position entfernen");
posButtons.add(btnPosHinzu);
posButtons.add(btnPosEntfernen);
btnPosHinzu.addActionListener(ev -> posModel.addRow(new Object[]{"1", "1"}));
btnPosEntfernen.addActionListener(ev -> {
int row = tblPositionen.getSelectedRow();
if (row >= 0) posModel.removeRow(row);
});
JPanel posPanel = new JPanel(new BorderLayout(4, 4));
posPanel.setBorder(BorderFactory.createTitledBorder("Positionen"));
posPanel.add(new JScrollPane(tblPositionen), BorderLayout.CENTER);
posPanel.add(posButtons, BorderLayout.SOUTH);
// --- OK / Abbrechen ---
JButton btnOk = new JButton("Anlegen");
JButton btnCancel = new JButton("Abbrechen");
JPanel dlgButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
dlgButtons.add(btnOk);
dlgButtons.add(btnCancel);
JLabel lblFehler = new JLabel(" ");
lblFehler.setForeground(new Color(192, 57, 43));
btnCancel.addActionListener(ev -> dialog.dispose());
btnOk.addActionListener(ev -> {
// Bei in der Tabelle laufender Bearbeitung: Wert übernehmen
if (tblPositionen.isEditing()) tblPositionen.getCellEditor().stopCellEditing();
// Kunden-ID parsen
long kundenId;
try {
kundenId = Long.parseLong(txtKundenIdInput.getText().trim());
} catch (NumberFormatException ex) {
lblFehler.setText("Kunden-ID muss eine Zahl sein.");
return;
}
// Positionen aufbauen
List<PositionCommand> positionen = new ArrayList<>();
for (int r = 0; r < posModel.getRowCount(); r++) {
Object pidObj = posModel.getValueAt(r, 0);
Object mengeObj = posModel.getValueAt(r, 1);
if (pidObj == null || mengeObj == null
|| pidObj.toString().isBlank() || mengeObj.toString().isBlank()) {
continue;
}
try {
long pid = Long.parseLong(pidObj.toString().trim());
int menge = Integer.parseInt(mengeObj.toString().trim());
positionen.add(new PositionCommand(pid, menge));
} catch (NumberFormatException ex) {
lblFehler.setText("Position " + (r + 1)
+ ": Produkt-ID und Menge müssen Zahlen sein.");
return;
} catch (IllegalArgumentException ex) {
lblFehler.setText("Position " + (r + 1) + ": " + ex.getMessage());
return;
}
}
if (positionen.isEmpty()) {
lblFehler.setText("Mindestens eine Position erforderlich.");
return;
}
// Angebot anlegen
try {
Angebot a = dokumentenService.createAngebot(kundenId, positionen);
ladeDokumente();
mainFrame.showStatus("Angebot " + a.getDokumentNummer()
+ " angelegt (Netto " + a.getNettoSumme() + " €).");
dialog.dispose();
} catch (Exception ex) {
lblFehler.setText("Fehler: " + ex.getMessage());
}
});
JPanel southPanel = new JPanel(new BorderLayout());
southPanel.add(lblFehler, BorderLayout.NORTH);
southPanel.add(dlgButtons, BorderLayout.SOUTH);
dialog.add(headPanel, BorderLayout.NORTH);
dialog.add(posPanel, BorderLayout.CENTER);
dialog.add(southPanel, BorderLayout.SOUTH);
dialog.setSize(480, 360);
dialog.setLocationRelativeTo(this);
dialog.setVisible(true);
}
}

View File

@ -3,29 +3,46 @@ package de.hsmannheim.faktura.gui;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.io.File;
import java.util.List;
import de.hsmannheim.faktura.kunde.Kunde;
import de.hsmannheim.faktura.kunde.service.KundenService;
import de.hsmannheim.faktura.kunde.repository.KundenRepository;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
import de.hsmannheim.faktura.kunde.model.Kunde;
import de.hsmannheim.faktura.kunde.repository.KundeRepository;
import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson;
import de.hsmannheim.faktura.kunde.service.BelegReferenzPruefer;
import de.hsmannheim.faktura.kunde.service.KundeService;
import de.hsmannheim.faktura.kunde.service.KundeValidator;
import de.hsmannheim.faktura.kunde.service.LoeschsperreException;
import de.hsmannheim.faktura.kunde.service.ValidierungsException;
/**
* Panel für die Kundenverwaltung (PHGUI02, PHGUI03, PHGUI05PHGUI09).
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
*
* Verdrahtung gegen die spec-konforme Schnittstelle K1 aus dem
* gemeinsamen Pflichtenheft v1.1 (Kap. 6.1.2 / Kap. 7.2):
* - Klassennamen: {@link KundeService}, {@link KundeRepository}, {@link Kunde}
* - Methoden: anlegen / bearbeiten / suchen / loeschen / finde / alle
* - Typ kundeId: long
*
* Das ursprüngliche GUI-Modell (KundenService / KundenRepository /
* 8-arg-Konstruktor / getKundenId():int) ist eine Erfindung des
* Gruppen-E-Pflichtenhefts und wird hier auf die gemeinsame Spec
* gemappt. Eintrag im Fahrplan: OP-6 (GUI vs. PH v1.1).
*/
public class KundenPanel extends JPanel {
private final MainFrame mainFrame;
private final KundenService kundenService;
private final KundeService kundeService;
// Tabelle
private DefaultTableModel tableModel;
private JTable kundenTable;
// Formularfelder
private JTextField txtKundenId;
private JTextField txtKundeId;
private JTextField txtFirmenname;
private JTextField txtNachname;
private JTextField txtVorname;
private JTextField txtStrasse;
private JTextField txtPlz;
private JTextField txtOrt;
@ -34,7 +51,7 @@ public class KundenPanel extends JPanel {
private JTextField txtUstIdNr;
// Fehlerhinweise (PHGUI03, PHGUI05)
private JLabel lblFirmenfehler;
private JLabel lblNameFehler;
private JLabel lblStrasseFehler;
private JLabel lblPlzFehler;
private JLabel lblOrtFehler;
@ -51,9 +68,10 @@ public class KundenPanel extends JPanel {
public KundenPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame;
this.kundenService = new KundenService(
new KundenRepository(),
createDummyDokumentRepository()
this.kundeService = new KundeService(
new KundeRepositoryJson(new File("data/kunden.json")),
new KundeValidator(),
stubBelegPruefer()
);
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@ -61,153 +79,183 @@ public class KundenPanel extends JPanel {
ladeKunden();
}
/**
* GR-05-Löschsperre lokal deaktiviert: das DokumentPanel hat eine
* eigene Repository-Instanz, eine geteilte Belegsicht erfordert ein
* zentrales Wiring (über MainFrame) und ist Bestandteil von OP-6.
*/
private BelegReferenzPruefer stubBelegPruefer() {
return kundeId -> false;
}
private void initComponents() {
// --- Linke Seite: Liste + Suche ---
// --- Linke Seite: Liste + Suche (PHGUI09) ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(320, 0));
String[] columns = {"ID", "Name ▲", "Ort"};
String[] columns = {"ID", "Name", "Ort"};
tableModel = new DefaultTableModel(columns, 0) {
public boolean isCellEditable(int r, int c) { return false; }
};
kundenTable = new JTable(tableModel);
kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Sortierung (PHGUI08)
kundenTable.setAutoCreateRowSorter(true);
kundenTable.getTableHeader().setReorderingAllowed(false);
kundenTable.setAutoCreateRowSorter(true); // PHGUI08
kundenTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehlten();
});
leftPanel.add(new JLabel("Kundenliste"), BorderLayout.NORTH);
leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER);
// Suche (PHGUI09: nach Name oder Kunden-ID)
JPanel suchePanel = new JPanel(new BorderLayout(4, 0));
suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche (Name / Kunden-ID)"));
JPanel suchPanel = new JPanel(new BorderLayout(4, 0));
suchPanel.add(new JLabel("Suche: "), BorderLayout.WEST);
txtSuche = new JTextField();
txtSuche.setToolTipText("Name oder ID eingeben...");
txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
});
suchePanel.add(txtSuche, BorderLayout.CENTER);
leftPanel.add(suchePanel, BorderLayout.SOUTH);
suchPanel.add(txtSuche, BorderLayout.CENTER);
// --- Rechte Seite: Formular ---
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
rightPanel.setBorder(BorderFactory.createTitledBorder("Kundendetails Pflichtfelder sind mit * markiert"));
leftPanel.add(suchPanel, BorderLayout.NORTH);
leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER);
JPanel formPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(3, 4, 1, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 0.5;
// Zeile 0: KundenId + Telefon
addLabel(formPanel, gbc, "Kunden-ID (autom.)", 0, 0);
addLabel(formPanel, gbc, "Telefonnummer", 1, 0);
txtKundenId = new JTextField(); txtKundenId.setEditable(false);
txtKundenId.setBackground(new Color(230, 230, 230));
addField(formPanel, gbc, txtKundenId, 0, 1);
txtTelefon = new JTextField();
addField(formPanel, gbc, txtTelefon, 1, 1);
// Zeile 2: Firmenname + E-Mail
addLabel(formPanel, gbc, "Firmenname / Nachname *", 0, 2);
addLabel(formPanel, gbc, "E-Mail", 1, 2);
txtFirmenname = new JTextField();
addField(formPanel, gbc, txtFirmenname, 0, 3);
txtEmail = new JTextField();
addField(formPanel, gbc, txtEmail, 1, 3);
// Fehler Firmenname + E-Mail
lblFirmenfehler = createFehlerLabel();
addField(formPanel, gbc, lblFirmenfehler, 0, 4);
lblEmailFehler = createFehlerLabel();
lblEmailFehler.setText("▲ Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120));
addField(formPanel, gbc, lblEmailFehler, 1, 4);
// Zeile 5: Straße + UStIdNr
addLabel(formPanel, gbc, "Straße *", 0, 5);
addLabel(formPanel, gbc, "UStIdNr.", 1, 5);
txtStrasse = new JTextField();
addField(formPanel, gbc, txtStrasse, 0, 6);
txtUstIdNr = new JTextField();
addField(formPanel, gbc, txtUstIdNr, 1, 6);
lblStrasseFehler = createFehlerLabel();
addField(formPanel, gbc, lblStrasseFehler, 0, 7);
// Zeile 8: PLZ
addLabel(formPanel, gbc, "PLZ *", 0, 8);
txtPlz = new JTextField();
addField(formPanel, gbc, txtPlz, 0, 9);
lblPlzFehler = createFehlerLabel();
addField(formPanel, gbc, lblPlzFehler, 0, 10);
// Zeile 11: Ort
addLabel(formPanel, gbc, "Ort *", 0, 11);
txtOrt = new JTextField();
addField(formPanel, gbc, txtOrt, 0, 12);
lblOrtFehler = createFehlerLabel();
addField(formPanel, gbc, lblOrtFehler, 0, 13);
rightPanel.add(new JScrollPane(formPanel), BorderLayout.CENTER);
// --- Buttons (PHGUI02) ---
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
btnNeu = new JButton("Neu");
btnSpeichern = new JButton("Speichern");
btnLoeschen = new JButton("Löschen");
btnAbbrechen = new JButton("Abbrechen");
btnPanel.add(btnNeu);
btnPanel.add(btnSpeichern);
btnPanel.add(btnLoeschen);
btnPanel.add(btnAbbrechen);
rightPanel.add(btnPanel, BorderLayout.SOUTH);
btnNeu.addActionListener(e -> neuesFormular());
btnSpeichern.addActionListener(e -> speichern());
btnLoeschen.addActionListener(e -> loeschen());
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeKunden(); });
// --- Rechte Seite: Formular + Buttons (PHGUI02) ---
JPanel rightPanel = new JPanel(new BorderLayout(0, 8));
rightPanel.add(formularPanel(), BorderLayout.CENTER);
rightPanel.add(buttonPanel(), BorderLayout.SOUTH);
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
split.setDividerLocation(320);
add(split, BorderLayout.CENTER);
}
// --- Hilfsmethoden Layout ---
private void addLabel(JPanel p, GridBagConstraints g, String text, int col, int row) {
g.gridx = col; g.gridy = row; g.weighty = 0;
p.add(new JLabel(text), g);
private JPanel formularPanel() {
JPanel p = new JPanel(new GridBagLayout());
p.setBorder(BorderFactory.createTitledBorder("Kundendaten"));
GridBagConstraints g = new GridBagConstraints();
g.insets = new Insets(4, 6, 2, 6);
g.anchor = GridBagConstraints.WEST;
g.fill = GridBagConstraints.HORIZONTAL;
g.weightx = 1.0;
txtKundeId = createReadOnlyField();
txtFirmenname = new JTextField();
txtNachname = new JTextField();
txtVorname = new JTextField();
txtStrasse = new JTextField();
txtPlz = new JTextField();
txtOrt = new JTextField();
txtTelefon = new JTextField();
txtEmail = new JTextField();
txtUstIdNr = new JTextField();
lblNameFehler = createFehlerLabel();
lblStrasseFehler = createFehlerLabel();
lblPlzFehler = createFehlerLabel();
lblOrtFehler = createFehlerLabel();
lblEmailFehler = createFehlerLabel();
lblEmailFehler.setText("ⓘ Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120));
int row = 0;
row = addLabelField(p, g, "Kunden-ID:", txtKundeId, null, row);
row = addLabelField(p, g, "Firmenname *:", txtFirmenname, lblNameFehler, row);
row = addLabelField(p, g, "Nachname *:", txtNachname, null, row);
row = addLabelField(p, g, "Vorname:", txtVorname, null, row);
row = addLabelField(p, g, "Straße + Nr. *:", txtStrasse, lblStrasseFehler, row);
row = addLabelField(p, g, "PLZ *:", txtPlz, lblPlzFehler, row);
row = addLabelField(p, g, "Ort *:", txtOrt, lblOrtFehler, row);
row = addLabelField(p, g, "Telefon:", txtTelefon, null, row);
row = addLabelField(p, g, "E-Mail:", txtEmail, lblEmailFehler, row);
row = addLabelField(p, g, "USt-IdNr.:", txtUstIdNr, null, row);
// Fülldummy, damit obere Felder oben bleiben
g.gridx = 0; g.gridy = row; g.weighty = 1.0;
p.add(new JLabel(" "), g);
JLabel hint = new JLabel("* Pflichtfeld Firmenname ODER Nachname ist Pflicht (PH v1.1 Kap. 6.1.2)");
hint.setFont(new Font("SansSerif", Font.ITALIC, 11));
hint.setForeground(new Color(120, 120, 120));
JPanel wrap = new JPanel(new BorderLayout());
wrap.add(p, BorderLayout.CENTER);
wrap.add(hint, BorderLayout.SOUTH);
return wrap;
}
private void addField(JPanel p, GridBagConstraints g, JComponent c, int col, int row) {
g.gridx = col; g.gridy = row; g.weighty = 0;
p.add(c, g);
private int addLabelField(JPanel p, GridBagConstraints g,
String label, JComponent field, JLabel fehlerLabel, int row) {
g.weighty = 0;
g.gridx = 0; g.gridy = row; g.weightx = 0; p.add(new JLabel(label), g);
g.gridx = 1; g.gridy = row; g.weightx = 1.0; p.add(field, g);
row++;
if (fehlerLabel != null) {
g.gridx = 1; g.gridy = row; g.weightx = 1.0; p.add(fehlerLabel, g);
row++;
}
return row;
}
private JPanel buttonPanel() {
JPanel p = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
btnNeu = new JButton("Neu");
btnSpeichern = new JButton("Speichern");
btnLoeschen = new JButton("Löschen");
btnAbbrechen = new JButton("Abbrechen");
btnNeu.addActionListener(e -> neuesFormular());
btnSpeichern.addActionListener(e -> speichern());
btnLoeschen.addActionListener(e -> loeschen());
btnAbbrechen.addActionListener(e -> { neuesFormular(); kundenTable.clearSelection(); });
p.add(btnNeu); p.add(btnSpeichern); p.add(btnLoeschen); p.add(btnAbbrechen);
return p;
}
// --- Daten ---
private void ladeKunden() {
ladeKundenListe(kundenService.getAllKunden());
ladeKundenListe(kundeService.alle());
}
private void ladeKundenListe(List<Kunde> liste) {
tableModel.setRowCount(0);
for (Kunde k : liste) {
tableModel.addRow(new Object[]{
String.valueOf(k.getKundenId()),
k.getFirmennameNachname(),
k.getOrt()
Long.toString(k.getKundeId()),
nameVon(k),
nullAls(k.getOrt())
});
}
}
/**
* Anzeigename gemäß PH v1.1 Kap. 6.1.2: firmenname ODER nachname ist
* Pflicht. Liefert firmenname, sonst nachname[, vorname].
*/
private static String nameVon(Kunde k) {
if (k.getFirmenname() != null && !k.getFirmenname().isBlank()) {
return k.getFirmenname();
}
String n = nullAls(k.getNachname());
if (k.getVorname() != null && !k.getVorname().isBlank()) {
return n + ", " + k.getVorname();
}
return n;
}
private static String nullAls(String s) { return s == null ? "" : s; }
private void sucheAktualisieren() {
// PHGUI09: Groß-/Kleinschreibung ignorieren, nach Name oder ID suchen
// PHGUI09: Treffer über Name UND Kundennummer.
// KundeService.suchen("") liefert bewusst eine leere Liste (P4-3),
// deshalb hier explizit auf .alle() umschalten.
String query = txtSuche.getText();
ladeKundenListe(kundenService.searchKunden(query));
if (query == null || query.isBlank()) {
ladeKundenListe(kundeService.alle());
} else {
ladeKundenListe(kundeService.suchen(query));
}
}
private void zeigeAusgewaehlten() {
@ -215,118 +263,144 @@ public class KundenPanel extends JPanel {
if (row < 0) return;
int modelRow = kundenTable.convertRowIndexToModel(row);
String idStr = (String) tableModel.getValueAt(modelRow, 0);
kundenService.getKundeById(Integer.parseInt(idStr)).ifPresent(k -> {
txtKundenId.setText(String.valueOf(k.getKundenId()));
txtFirmenname.setText(k.getFirmennameNachname());
txtStrasse.setText(k.getStrasse());
txtPlz.setText(k.getPlz());
txtOrt.setText(k.getOrt());
txtTelefon.setText(k.getTelefonnummer());
txtEmail.setText(k.getEmail());
txtUstIdNr.setText(k.getUstIdNr());
clearFehler();
});
try {
long kundeId = Long.parseLong(idStr);
kundeService.finde(kundeId).ifPresent(this::fuelleFormular);
} catch (NumberFormatException ignored) {
// Tabelle in inkonsistentem Zustand; einfach ignorieren
}
}
private void fuelleFormular(Kunde k) {
txtKundeId.setText(Long.toString(k.getKundeId()));
txtFirmenname.setText(nullAls(k.getFirmenname()));
txtNachname.setText(nullAls(k.getNachname()));
txtVorname.setText(nullAls(k.getVorname()));
txtStrasse.setText(nullAls(k.getStrasse()));
txtPlz.setText(nullAls(k.getPlz()));
txtOrt.setText(nullAls(k.getOrt()));
txtTelefon.setText(nullAls(k.getTelefon()));
txtEmail.setText(nullAls(k.getEmail()));
txtUstIdNr.setText(nullAls(k.getUstIdNr()));
clearFehler();
}
// --- Aktionen ---
private void neuesFormular() {
txtKundenId.setText(""); txtFirmenname.setText(""); txtStrasse.setText("");
txtPlz.setText(""); txtOrt.setText(""); txtTelefon.setText("");
txtEmail.setText(""); txtUstIdNr.setText("");
txtKundeId.setText("");
txtFirmenname.setText(""); txtNachname.setText(""); txtVorname.setText("");
txtStrasse.setText(""); txtPlz.setText(""); txtOrt.setText("");
txtTelefon.setText(""); txtEmail.setText(""); txtUstIdNr.setText("");
clearFehler();
kundenTable.clearSelection();
}
private void speichern() {
clearFehler();
boolean valid = true;
// PHGUI03: Pflichtfelder
if (txtFirmenname.getText().trim().isEmpty()) {
lblFirmenfehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
}
if (txtStrasse.getText().trim().isEmpty()) {
lblStrasseFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
}
if (txtPlz.getText().trim().isEmpty()) {
lblPlzFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
}
if (txtOrt.getText().trim().isEmpty()) {
lblOrtFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
// GUI-seitige Vorprüfung der Pflichtfelder (PHGUI03) - die fachliche
// Validierung gegen PH v1.1 macht der KundeValidator beim Service-Aufruf.
boolean clientValid = true;
if (istLeer(txtFirmenname) && istLeer(txtNachname)) {
lblNameFehler.setText("⚠ Firmenname ODER Nachname erforderlich");
clientValid = false;
}
if (istLeer(txtStrasse)) { lblStrasseFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
if (istLeer(txtPlz)) { lblPlzFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
if (istLeer(txtOrt)) { lblOrtFehler.setText("⚠ Pflichtfeld"); clientValid = false; }
if (!clientValid) return;
// PHGUI05: E-Mail-Validierung (Format: name@domain.tld)
String email = txtEmail.getText().trim();
if (!email.isEmpty() && !email.matches("^[^@]+@[^@]+\\.[^@]+$")) {
lblEmailFehler.setText("⚠ Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(192, 57, 43));
valid = false;
// Kunde-Objekt aufbauen (no-arg-Konstruktor + Setter; Jackson-kompatibel)
Kunde k = new Kunde();
if (!txtKundeId.getText().isBlank()) {
try { k.setKundeId(Long.parseLong(txtKundeId.getText().trim())); }
catch (NumberFormatException ignored) { /* bleibt 0 = Neuanlage */ }
}
if (!valid) return;
int kundenId = txtKundenId.getText().trim().isEmpty() ? 0
: Integer.parseInt(txtKundenId.getText().trim());
Kunde k = new Kunde(
kundenId,
txtFirmenname.getText().trim(),
txtStrasse.getText().trim(),
txtPlz.getText().trim(),
txtOrt.getText().trim(),
txtTelefon.getText().trim(),
email,
txtUstIdNr.getText().trim()
);
k.setFirmenname(leerAlsNull(txtFirmenname));
k.setNachname(leerAlsNull(txtNachname));
k.setVorname(leerAlsNull(txtVorname));
k.setStrasse(leerAlsNull(txtStrasse));
k.setPlz(leerAlsNull(txtPlz));
k.setOrt(leerAlsNull(txtOrt));
k.setTelefon(leerAlsNull(txtTelefon));
k.setEmail(leerAlsNull(txtEmail));
k.setUstIdNr(leerAlsNull(txtUstIdNr));
try {
kundenService.saveKunde(k);
if (k.getKundeId() == 0) {
long neueId = kundeService.anlegen(k); // F-KV-01
k.setKundeId(neueId);
} else {
kundeService.bearbeiten(k); // F-KV-02
}
ladeKunden();
mainFrame.showStatus("Kunde gespeichert (ID " + k.getKundeId() + ")."); // PHGUI06
neuesFormular();
// PHGUI06: Bestätigung < 1 Sekunde
mainFrame.showStatus("Kunde erfolgreich gespeichert.");
} catch (IllegalArgumentException ex) {
mainFrame.showStatus("Fehler: " + ex.getMessage());
} catch (ValidierungsException ex) {
// Fehler aus KundeValidator (PH v1.1 Kap. 6.1.2) sichtbar machen
String msg = ex.getMessage();
if (msg.toLowerCase().contains("email") || msg.toLowerCase().contains("e-mail")) {
lblEmailFehler.setText("⚠ " + msg);
lblEmailFehler.setForeground(new Color(192, 57, 43));
} else if (msg.toLowerCase().contains("firmen") || msg.toLowerCase().contains("nach")) {
lblNameFehler.setText("⚠ " + msg);
} else if (msg.toLowerCase().contains("plz")) {
lblPlzFehler.setText("⚠ " + msg);
} else if (msg.toLowerCase().contains("straße") || msg.toLowerCase().contains("strasse")) {
lblStrasseFehler.setText("⚠ " + msg);
} else if (msg.toLowerCase().contains("ort")) {
lblOrtFehler.setText("⚠ " + msg);
} else {
mainFrame.showStatus("Validierung: " + msg);
}
}
}
private void loeschen() {
int row = kundenTable.getSelectedRow();
if (row < 0) {
mainFrame.showStatus("Kein Kunde ausgewählt.");
return;
}
if (row < 0) { mainFrame.showStatus("Kein Kunde ausgewählt."); return; }
// PHGUI07: Bestätigungsdialog
int confirm = JOptionPane.showConfirmDialog(
this, "Kunden wirklich löschen?", "Löschen bestätigen",
JOptionPane.YES_NO_OPTION
);
this, "Kunden wirklich löschen?", "Löschen bestätigen",
JOptionPane.YES_NO_OPTION);
if (confirm != JOptionPane.YES_OPTION) return;
int modelRow = kundenTable.convertRowIndexToModel(row);
String idStr = (String) tableModel.getValueAt(modelRow, 0);
boolean deleted = kundenService.deleteKunde(Integer.parseInt(idStr));
if (deleted) {
try {
long kundeId = Long.parseLong(idStr);
kundeService.loeschen(kundeId); // F-KV-04
ladeKunden();
neuesFormular();
mainFrame.showStatus("Kunde gelöscht.");
} else {
mainFrame.showStatus("Löschen nicht möglich Kunde wird in Dokumenten verwendet.");
} catch (LoeschsperreException ex) {
mainFrame.showStatus("Löschen nicht möglich " + ex.getMessage()); // GR-05
} catch (ValidierungsException ex) {
mainFrame.showStatus("Fehler: " + ex.getMessage());
} catch (NumberFormatException ex) {
mainFrame.showStatus("Ungültige Kunden-ID.");
}
}
// --- Hilfsmethoden ---
private boolean istLeer(JTextField f) {
return f.getText() == null || f.getText().trim().isEmpty();
}
private String leerAlsNull(JTextField f) {
String s = f.getText() == null ? null : f.getText().trim();
return (s == null || s.isEmpty()) ? null : s;
}
private void clearFehler() {
lblFirmenfehler.setText("");
lblNameFehler.setText("");
lblStrasseFehler.setText("");
lblPlzFehler.setText("");
lblOrtFehler.setText("");
lblEmailFehler.setText("▲ Format: name@domain.tld");
lblEmailFehler.setText(" Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120));
}
@ -337,15 +411,10 @@ public class KundenPanel extends JPanel {
return lbl;
}
private DokumentRepository createDummyDokumentRepository() {
return new DokumentRepository() {
public boolean existsReferencedProdukt(long id) { return false; }
public boolean existsReferencedKunde(long id) { return false; }
public int findMaxLaufendeNummer(String p, int j) { return 0; }
public void save(de.hsmannheim.faktura.dokumente.domain.Dokument d) {}
public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return List.of(); }
public boolean existsFolgedokument(String q) { return false; }
};
private JTextField createReadOnlyField() {
JTextField f = new JTextField();
f.setEditable(false);
f.setBackground(new Color(230, 230, 230));
return f;
}
}

View File

@ -49,8 +49,8 @@ public class ProduktPanel extends JPanel {
public ProduktPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame;
this.productService = new ProductService(
new ProductRepository(),
createDummyDokumentRepository()
new ProductRepository(),
createDummyDokumentRepository()
);
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@ -192,9 +192,9 @@ public class ProduktPanel extends JPanel {
tableModel.setRowCount(0);
for (Product p : liste) {
tableModel.addRow(new Object[]{
p.getProductId(),
p.getBezeichnung(),
p.getEinzelpreisNetto() + " €"
p.getProductId(),
p.getBezeichnung(),
p.getEinzelpreisNetto() + " €"
});
}
}
@ -267,12 +267,12 @@ public class ProduktPanel extends JPanel {
if (!valid) return;
Product p = new Product(
txtProduktId.getText().trim(),
bezeichnung,
preis,
(MwstSatz) cmbMwst.getSelectedItem(),
txtBeschreibung.getText().trim(),
txtKategorie.getText().trim()
txtProduktId.getText().trim(),
bezeichnung,
preis,
(MwstSatz) cmbMwst.getSelectedItem(),
txtBeschreibung.getText().trim(),
txtKategorie.getText().trim()
);
try {
@ -295,10 +295,10 @@ public class ProduktPanel extends JPanel {
// PHGUI07: Bestätigungsdialog
int confirm = JOptionPane.showConfirmDialog(
this,
"Produkt wirklich löschen?",
"Löschen bestätigen",
JOptionPane.YES_NO_OPTION
this,
"Produkt wirklich löschen?",
"Löschen bestätigen",
JOptionPane.YES_NO_OPTION
);
if (confirm != JOptionPane.YES_OPTION) return;
@ -337,6 +337,8 @@ public class ProduktPanel extends JPanel {
public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return List.of(); }
public boolean existsFolgedokument(String q) { return false; }
public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findAll() { return List.of(); }
public boolean delete(String n) { return false; }
};
}
}
}

View File

@ -34,24 +34,26 @@ public class ProductPerformanceTest {
@Override public java.util.Optional<de.hsmannheim.faktura.dokumente.domain.Dokument> findByNummer(String n) { return java.util.Optional.empty(); }
@Override public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findByKunde(long id) { return java.util.List.of(); }
@Override public boolean existsFolgedokument(String q) { return false; }
@Override public List<de.hsmannheim.faktura.dokumente.domain.Dokument> findAll() { return java.util.List.of(); }
@Override public boolean delete(String n) { return false; }
};
ProductService service = new ProductService(repo, dummyDocRepo);
// Bereinigen und künstliches Befüllen mit exakt 1.000 Produkten
for (Product p : repo.findAll()) {
repo.deleteById(p.getProductId());
}
for (int i = 1; i <= 1000; i++) {
repo.save(new Product("P-" + i, "Produkt Nummer " + i, new BigDecimal("10.00"), MwstSatz.NORMAL, "", "Kategorie"));
}
// Zeitmessung starten
long startTime = System.currentTimeMillis();
List<Product> results = service.searchProducts("Nummer 999");
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;

View File

@ -32,11 +32,11 @@ public class ProductServiceTest {
@BeforeEach
public void setUp() {
productRepository = new ProductRepository();
dokumentRepository = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() {
@Override
public boolean existsReferencedProdukt(long produktId) {
return false;
return false;
}
@Override
@ -68,10 +68,20 @@ public class ProductServiceTest {
public boolean existsFolgedokument(String quellDokumentNummer) {
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
}
@Override
public List<Dokument> findAll() {
throw new UnsupportedOperationException("Unimplemented method 'findAll'");
}
@Override
public boolean delete(String nummer) {
throw new UnsupportedOperationException("Unimplemented method 'delete'");
}
};
productService = new ProductService(productRepository, dokumentRepository);
for (Product p : productRepository.findAll()) {
productRepository.deleteById(p.getProductId());
}
@ -84,7 +94,7 @@ public class ProductServiceTest {
@Test
public void testSaveValidProduct() {
Product valid = new Product("P-0001", "M6 Schraube", new BigDecimal("10.50"), MwstSatz.NORMAL, "Beschreibung", "Werkzeug");
assertDoesNotThrow(() -> productService.saveProduct(valid));
assertTrue(productService.getProductById("P-0001").isPresent());
}
@ -115,7 +125,7 @@ public class ProductServiceTest {
@Test
public void testNegativePriceRejection() {
Product invalidProduct = new Product("P-9999", "Testprodukt", new BigDecimal("-1.00"), MwstSatz.NORMAL, "Test", "Test");
assertThrows(IllegalArgumentException.class, () -> {
productService.saveProduct(invalidProduct);
});
@ -162,10 +172,10 @@ public class ProductServiceTest {
public void testSearchCaseInsensitive() {
Product p1 = new Product("P-0001", "M6 Schraube", new BigDecimal("1.00"), MwstSatz.NORMAL, "", "");
Product p2 = new Product("P-0002", "M8 SCHRAUBE", new BigDecimal("2.00"), MwstSatz.NORMAL, "", "");
productService.saveProduct(p1);
productService.saveProduct(p2);
List<Product> results = productService.searchProducts("schraube");
assertEquals(2, results.size());
}
@ -178,16 +188,16 @@ public class ProductServiceTest {
public void testDeleteReferencedProduct() {
de.hsmannheim.faktura.dokumente.repository.DokumentRepository mockRepo = new de.hsmannheim.faktura.dokumente.repository.DokumentRepository() {
@Override
public boolean existsReferencedProdukt(long produktId) {
return true;
}
@Override
public boolean existsReferencedKunde(long kundenId) {
return false;
public boolean existsReferencedProdukt(long produktId) {
return true;
}
@Override
public int findMaxLaufendeNummer(String praefix, int jahr) {
return 0;
public boolean existsReferencedKunde(long kundenId) {
return false;
}
@Override
public int findMaxLaufendeNummer(String praefix, int jahr) {
return 0;
}
@Override
public void save(Dokument dokument) {
@ -205,8 +215,16 @@ public class ProductServiceTest {
public boolean existsFolgedokument(String quellDokumentNummer) {
throw new UnsupportedOperationException("Unimplemented method 'existsFolgedokument'");
}
@Override
public List<Dokument> findAll() {
throw new UnsupportedOperationException("Unimplemented method 'findAll'");
}
@Override
public boolean delete(String nummer) {
throw new UnsupportedOperationException("Unimplemented method 'delete'");
}
};
ProductService serviceWithSperre = new ProductService(productRepository, mockRepo);
Product product = new Product("P-0001", "Gesperrtes Produkt", new BigDecimal("10.00"), MwstSatz.NORMAL, "", "");
productRepository.save(product);