Compare commits
12 Commits
feature/pr
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
469acd2060 | |
|
|
576220c381 | |
|
|
d73359ffc2 | |
|
|
945188748e | |
|
|
92dbd53986 | |
|
|
9f22667592 | |
|
|
54c1a482b4 | |
|
|
51619ceb8f | |
|
|
d0a53379db | |
|
|
66085831b8 | |
|
|
f925d26c86 | |
|
|
2483d51cb7 |
|
|
@ -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. |
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
[ {
|
||||
"productId" : "P-0003",
|
||||
"bezeichnung" : "Bohrmaschine",
|
||||
"einzelpreisNetto" : 89.99,
|
||||
"mwstSatz" : "NORMAL",
|
||||
"beschreibung" : "Schlagbohrer",
|
||||
"kategorie" : "Elektrowerkzeuge"
|
||||
} ]
|
||||
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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(); }
|
||||
}
|
||||
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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, PHGUI05–PHGUI09).
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue