Compare commits

...

10 Commits

9 changed files with 2015 additions and 9 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,395 @@
# Pflichtenheft - Dokumentenprozess Fakturierungssystem
**Gruppe F - SE1 Team 2**
**Version:** 1.0
**Datum:** 11.06.2026
---
## Inhaltsverzeichnis
- [Freigabeübersicht](#freigabeübersicht)
- [Dokumentenhistorie](#dokumentenhistorie)
- [1. Einleitung](#1-einleitung)
- [1.1 Zweck dieses Dokuments](#11-zweck-dieses-dokuments)
- [1.2 Bezug zum Project Charter](#12-bezug-zum-project-charter)
- [1.3 Geltungsbereich](#13-geltungsbereich)
- [2. Systemüberblick für Gruppe F](#2-systemüberblick-für-gruppe-f)
- [3. Stakeholder und Kontext](#3-stakeholder-und-kontext)
- [4. Funktionale Anforderungen](#4-funktionale-anforderungen)
- [5. Nicht-funktionale Anforderungen für das Modul](#5-nicht-funktionale-anforderungen-für-das-modul)
- [6. Daten und Schnittstellen](#6-daten-und-schnittstellen)
- [6.1 Datenobjekte und Java-Datentypen](#61-datenobjekte-und-java-datentypen)
- [6.2 Schnittstellen](#62-schnittstellen)
- [7. Systemarchitektur](#7-systemarchitektur)
- [8. Testbare Abnahmekriterien](#8-testbare-abnahmekriterien)
- [9. Traceability zwischen Lastenheft und Pflichtenheft](#9-traceability-zwischen-lastenheft-und-pflichtenheft)
- [10. Modultestplan](#10-modultestplan)
- [11. Glossar](#11-glossar)
- [12. Referenzen](#12-referenzen)
---
## Freigabeübersicht
| Ersteller | Prüfer | Freigebender |
|---|---|---|
| Gruppe F: Andreas Ivanovic, Armin Omanovic, Alexander Teller | Prof. Dr. Gerd Marmitt | SE1 Team 2 |
| SE1 Team 2 | Hochschule Mannheim | SE1 Team 2 |
| 11.06.2026 | [Datum Review] | 30.06.2026 |
---
## Dokumentenhistorie
| Version | Datum | Autor | Änderung |
|---|---:|---|---|
| 1.0 | 11.06.2026 | Gruppe F / Alexander Teller | Initiale Erstellung des Pflichtenheft-Teils und Modultestplans für den Dokumentenprozess |
---
# 1. Einleitung
## 1.1 Zweck dieses Dokuments
Dieses Pflichtenheft beschreibt die konkrete Umsetzung des Moduls **Dokumentenprozess** im Fakturierungssystem. Es konkretisiert die Anforderungen aus dem Lastenheft und dem Project Charter für den Verantwortungsbereich der Gruppe F. Der Fokus liegt auf der fachlichen Kette:
```text
Angebot -> Auftragsbestätigung -> Lieferschein -> Rechnung
```
Das Dokument dient als Input für Implementierung, Komponententests und Integration mit Produktverwaltung, Kundenverwaltung und GUI.
## 1.2 Bezug zum Project Charter
Team 2 besteht aus den Gruppen E, F, G und H. Gruppe F ist laut Project Charter für den Dokumentenprozess verantwortlich. Der Dokumentenprozess umfasst die fachliche Kette **Angebot -> Auftragsbestätigung -> Lieferschein -> Rechnung**. Das Projekt nutzt **Java**, **JavaFX**, **SQLite** und **Gitty** und orientiert sich am **V-Modell**.
## 1.3 Geltungsbereich
Dieses Pflichtenheft deckt nur den Dokumentenprozess ab. Produktverwaltung, Kundenverwaltung und GUI werden nur als Schnittstellen beschrieben, weil sie durch die Gruppen G, H und E verantwortet werden.
Nicht Teil dieses Moduls sind:
- vollständige Buchhaltung
- E-Rechnung
- Mehrbenutzerbetrieb
- Cloud-Betrieb
- externe ERP-/FiBu-Anbindungen
---
# 2. Systemüberblick für Gruppe F
Das Modul Gruppe F stellt die Geschäftslogik für Dokumente bereit. Die GUI ruft den `DokumentService` auf. Produkt- und Kundendaten werden über Lookup-Schnittstellen gelesen. Dokumente werden persistent über ein Repository gespeichert. Aus vorhandenen Dokumenten können Folgedokumente erzeugt werden, wobei Status, Referenzen und fachliche Regeln geprüft werden.
---
# 3. Stakeholder und Kontext
| Stakeholder / Rolle | Interesse am Modul Dokumentenprozess |
|---|---|
| Auftraggeber Prof. Dr. Gerd Marmitt | Nachvollziehbare Umsetzung der Anforderungen und testbare Artefakte im V-Modell. |
| Gruppe F | Implementierung des Dokumentenworkflows und Bereitstellung testbarer Services. |
| Gruppe E - GUI | Benötigt Service-Methoden und DTOs für Anzeige, Eingabe und Export. |
| Gruppe G - Produktverwaltung | Liefert Produktdaten und muss prüfen können, ob Produkte in Dokumenten referenziert sind. |
| Gruppe H - Kundenverwaltung | Liefert Kundendaten und muss prüfen können, ob Kunden in Dokumenten referenziert sind. |
| Endnutzer | Möchte Angebote, Aufträge, Lieferscheine und Rechnungen korrekt erzeugen und wiederfinden. |
---
# 4. Funktionale Anforderungen
| PH-ID | LH-Quelle | Kurzname | Konkretisierung / Systemverhalten | Priorität | Abnahmekriterium |
|---|---|---|---|---|---|
| PH-DP-01 | BA-DP-01, GR-04, GR-06 | Angebot anlegen | Das System MUSS über `DokumentService.erstelleAngebot(long kundenId, List<PositionCommand> positionen)` ein Angebot für genau einen vorhandenen Kunden mit mindestens einer Position erzeugen. Pro Position werden Produktbezeichnung, Einzelpreis netto und MwSt.-Satz als Snapshot übernommen. | Muss | Angebot ist gespeichert, besitzt Nummer `A-yyyy-nnnn`, Status `OFFEN`, Datum `LocalDate.now()` und korrekt berechnete Netto-, USt.- und Bruttosumme. |
| PH-DP-02 | F-DP-02 | Angebotsnummer vergeben | Das System MUSS Angebotsnummern über `OfferNumberGenerator` je Kalenderjahr fortlaufend und eindeutig vergeben. | Muss | Zwei nacheinander erzeugte Angebote erhalten unterschiedliche und aufeinanderfolgende Nummern. |
| PH-DP-03 | F-DP-03 | Angebot bearbeiten | Das System MUSS die Bearbeitung eines Angebots nur erlauben, solange `AngebotsStatus.OFFEN` gilt. | Muss | Bearbeitung bei `OFFEN` erfolgreich; bei `UEBERFUEHRT` wird `IllegalStateException` geworfen. |
| PH-DP-04 | BA-DP-04, GR-01 | Auftragsbestätigung erzeugen | Das System MUSS aus genau einem offenen Angebot eine Auftragsbestätigung mit eigener Nummer `AB-yyyy-nnnn` erzeugen und alle Positionen unverändert übernehmen. | Muss | AB referenziert Angebotsnummer, Positionen und Summen entsprechen dem Angebot, Angebot wird auf `UEBERFUEHRT` gesetzt. |
| PH-DP-05 | F-DP-05 | Doppelte Auftragsbestätigung verhindern | Das System MUSS vor der Erzeugung prüfen, ob für das Angebot bereits eine Auftragsbestätigung existiert. | Muss | Zweiter Erzeugungsversuch wird mit `DuplicateDocumentException` abgelehnt. |
| PH-DP-06 | BA-DP-06, GR-01 | Lieferschein erzeugen | Das System MUSS aus genau einer bestätigten Auftragsbestätigung einen Lieferschein mit Nummer `LS-yyyy-nnnn` und `LocalDate lieferdatum` erzeugen. | Muss | Lieferschein referenziert AB, übernimmt Produktbezeichnung und Mengen und erhält Status `GELIEFERT`. |
| PH-DP-07 | F-DP-07 | Lieferschein ohne Preise | Das System MUSS für Lieferscheinansicht und PDF nur Menge und Produktbezeichnung ausgeben, aber keine Einzelpreise, Netto-, USt.- oder Bruttowerte. | Muss | Export/DTO des Lieferscheins enthält keine Preisfelder. |
| PH-DP-08 | BA-DP-08, F-DP-09, GR-01 | Rechnung erzeugen | Das System MUSS aus genau einem gelieferten Lieferschein eine Rechnung mit Nummer `R-yyyy-nnnn`, Rechnungsdatum, Referenz auf den Lieferschein und Pflichtangaben nach UStG erzeugen. | Muss | Rechnung enthält Kunde, Leistungsdatum/Lieferdatum, Positionen, fortlaufende Nummer, Netto-, USt.- und Bruttosumme. |
| PH-DP-09 | F-DP-09 | Pflichtangaben prüfen | Das System MUSS vor dem Speichern einer Rechnung `RechnungPflichtangabenValidator.validate(Rechnung)` ausführen. | Muss | Fehlende Pflichtangaben verhindern Speicherung und liefern Validierungsfehler. |
| PH-DP-10 | F-DP-10, GR-02 | Rechnungsnummer fortlaufend | Das System MUSS Rechnungsnummern fortlaufend, eindeutig und lückenlos vergeben, sobald eine Rechnung erfolgreich gespeichert wird. | Muss | Drei erfolgreiche Rechnungen ergeben `R-2026-0001`, `R-2026-0002`, `R-2026-0003`. |
| PH-DP-11 | F-DP-11, GR-03 | Rechnung festschreiben | Das System MUSS eine gespeicherte Rechnung inhaltlich sperren (`festgeschrieben=true`). | Muss | Änderungsversuche an Positionen, Summen oder Pflichtangaben werden abgelehnt. |
| PH-DP-12 | F-DP-12 | PDF-Export | Das System SOLL jedes Dokument über `PdfExportService.export(Dokument)` als PDF-Datei im lokalen Dateisystem erzeugen. | Soll | PDF-Datei existiert, ist nicht leer und enthält Dokumentnummer und Dokumenttyp. |
| PH-DP-13 | F-DP-13 | Dokumentenübersicht pro Kunde | Das System SOLL über `DokumentRepository.findByKunde(long kundenId)` alle Dokumente eines Kunden sortiert nach Datum und Dokumenttyp bereitstellen. | Soll | Alle Angebote, ABs, Lieferscheine und Rechnungen des Kunden werden ohne fremde Kundendokumente geliefert. |
| PH-DP-14 | GR-04 | Preis-Snapshot sichern | Das System MUSS den Produktpreis und MwSt.-Satz beim Einfügen in das Angebot als Snapshot in `Dokumentposition` speichern. | Muss | Spätere Produktänderung beeinflusst bestehende Dokumentpositionen nicht. |
| PH-DP-15 | GR-05 | Stammdatenreferenzen prüfbar machen | Das Dokumentenmodul MUSS für Produkt- und Kundenverwaltung eine Prüffunktion `isReferenced(...)` bereitstellen. | Muss | Referenzierte Produkte/Kunden können durch andere Module als verwendet erkannt werden. |
| PH-DP-16 | NF-TEST-02, NF-MAINT-03 | Testbare Logik ohne GUI | Die Dokumentenlogik MUSS in Service- und Domainklassen ohne JavaFX-Abhängigkeit implementiert werden. | Muss | JUnit-Tests können `DokumentService` ohne Start der GUI ausführen. |
---
# 5. Nicht-funktionale Anforderungen für das Modul
| PH-NF-ID | LH-Quelle | Konkretisierung | Nachweis |
|---|---|---|---|
| PH-NF-DP-01 | NF-PERF-01 | Dokumentenaktionen bis 1.000 Kunden/Produkte müssen innerhalb von 1 Sekunde eine Rückmeldung liefern, wenn Repositories mit Testdaten initialisiert sind. | Zeitmessung im Komponententest/Integrationstest `< 1 s` für Erzeugung eines Angebots mit 10 Positionen. |
| PH-NF-DP-02 | NF-ARCH-01 | Dokumente müssen nach Neustart aus SQLite/Dateispeicher vollständig wiederherstellbar sein. | Dokument erstellen, Repository neu initialisieren, Dokument unverändert laden. |
| PH-NF-DP-03 | NF-ARCH-02 | Datenhaltung des Dokumentenmoduls muss über `DokumentRepository` gekapselt sein. | Code-Review: Service nutzt Interface, keine direkten SQL-Aufrufe im Service. |
| PH-NF-DP-04 | NF-MAINT-01, NF-MAINT-03 | Dokumentenprozess wird im Paket `de.hsma.se1.fakturierung.dokumente` umgesetzt und in Domain, Service, Repository und Export getrennt. | Repository-Struktur enthält getrennte Pakete; Abhängigkeiten laufen von UI -> Service -> Repository. |
| PH-NF-DP-05 | NF-TEST-02, NF-TEST-03 | Die Geschäftslogik des Dokumentenmoduls muss automatisiert testbar sein. | Mindestens 10 JUnit-Testfälle für Services/Domainklassen laufen ohne GUI. |
---
# 6. Daten und Schnittstellen
## 6.1 Datenobjekte und Java-Datentypen
| Datenfeld / Objekt | Java-Datentyp | Wertebereich / Format | Beschreibung |
|---|---|---|---|
| `DokumentType` | `enum` | `ANGEBOT`, `AUFTRAGSBESTAETIGUNG`, `LIEFERSCHEIN`, `RECHNUNG` | Typisierung für Anzeige, Export und Repository. |
| Dokumentnummer | `String` | Format `A-2026-0001`, `AB-2026-0001`, `LS-2026-0001`, `R-2026-0001` | Fachlicher Identifikator; nicht als `int`, da Präfix/Jahr enthalten. |
| `kundenId` | `long` | z. B. `10001L` | Referenz auf Kundenverwaltung, keine Kopie des gesamten Kundenobjekts im Dokumentservice erforderlich. |
| `produktId` | `long` | z. B. `20001L` | Referenz auf Produktverwaltung. |
| `bezeichnungSnapshot` | `String` | max. 255 Zeichen | Produktbezeichnung zum Zeitpunkt der Angebotsanlage. |
| `menge` | `int` | `> 0` | Menge pro Dokumentposition. |
| `einzelpreisNettoSnapshot` | `BigDecimal` | `>= 0`, Skalierung 2 | Geldbetrag; `BigDecimal` statt `double` wegen Rundungsfehlern. |
| `mwstSatzSnapshot` | `BigDecimal` | z. B. `0.19`, `0.07` | MwSt.-Satz als Dezimalwert. |
| `datum`, `lieferdatum`, `rechnungsdatum` | `LocalDate` | ISO-Format `yyyy-MM-dd` | Fachliche Datumswerte ohne Uhrzeit. |
| `nettoSumme`, `ustSumme`, `bruttoSumme` | `BigDecimal` | Skalierung 2, Rundung `HALF_UP` | Berechnete Summen des Dokuments. |
| `status` | `enum` | `OFFEN`, `UEBERFUEHRT`, `BESTAETIGT`, `GELIEFERT`, `FAKTURIERT`, `FESTGESCHRIEBEN` | Steuert erlaubte Folgeaktionen. |
| `pflichtangaben` | `RechnungPflichtangaben` | Objekt mit `leistungserbringerName`, `anschrift`, `steuerNr`, `leistungsdatum` usw. | Pflichtfelder der Rechnung. |
## 6.2 Schnittstellen
| Schnittstelle | Art | Zweck | Operationen / Daten |
|---|---|---|---|
| `DokumentService` | Java-Service-Interface | UI/Controller ruft Dokumentenlogik auf. | Methoden: `erstelleAngebot`, `erstelleAuftragsbestaetigung`, `erstelleLieferschein`, `erstelleRechnung`, `exportiereAlsPdf`. |
| `DokumentRepository` | Java-Interface / SQLite-Implementierung | Persistenz von Dokumenten. | `save`, `findByNummer`, `findByKunde`, `existsFolgedokument`, `existsReferencedProdukt`, `existsReferencedKunde`. |
| `ProductLookup` | Java-Interface zu Gruppe G | Produktdaten für Angebotspositionen lesen. | `findProduct(long produktId): Optional<ProductDto>` mit `String bezeichnung`, `BigDecimal einzelpreisNetto`, `BigDecimal mwstSatz`. |
| `CustomerLookup` | Java-Interface zu Gruppe H | Kundendaten für Dokumentkopf und Rechnungspflichtangaben lesen. | `findCustomer(long kundenId): Optional<CustomerDto>`. |
| `PdfExportService` | Java-Service / lokale Datei-Schnittstelle | PDF-Dateien aus Dokumentdaten erzeugen. | Rückgabe `Path`; Zielordner z. B. `/exports`. |
---
# 7. Systemarchitektur
Das Dokumentenmodul folgt einer dreischichtigen Struktur: Präsentation/UI ruft Services auf, Services enthalten die Geschäftslogik, Repositories kapseln die Datenhaltung. Die fachlichen Domänenklassen sind von JavaFX unabhängig und können isoliert getestet werden.
## Abbildung 1: UML-Klassendiagramm Gruppe F
```mermaid
classDiagram
direction TB
class DokumentService {
+erstelleAngebot(...) Angebot
+aktualisiereAngebot(...) Angebot
+erstelleAuftragsbestaetigung(...) Auftragsbestaetigung
+erstelleLieferschein(...) Lieferschein
+erstelleRechnung(...) Rechnung
+exportiereAlsPdf(...) Path
}
class Dokument {
<<abstract>>
-nummer String
-kundeId long
-datum LocalDate
-positionen List~Dokumentposition~
-nettoSumme BigDecimal
-ustSumme BigDecimal
-bruttoSumme BigDecimal
+berechneSummen() SummenDto
}
class Dokumentposition {
-produktId long
-bezeichnungSnapshot String
-menge int
-einzelpreisNettoSnapshot BigDecimal
-mwstSatzSnapshot BigDecimal
+berechneNetto() BigDecimal
+berechneUst() BigDecimal
}
class Angebot {
-angebotsNr String
-status AngebotsStatus
+istBearbeitbar() boolean
}
class Auftragsbestaetigung {
-auftragNr String
-angebotNr String
-status AuftragsStatus
+ausAngebot(...) ...
}
class Lieferschein {
-lieferscheinNr String
-auftragsNr String
-lieferdatum LocalDate
-status LieferStatus
}
class Rechnung {
-rechnungsNr String
-lieferscheinNr String
-rechnungsdatum LocalDate
-status RechnungsStatus
-festgeschrieben boolean
}
class DokumentRepository {
<<interface>>
+save(Dokument) Dokument
+findByNummer(String) Optional~Dokument~
+existsFolgedokument(String) boolean
+findByKunde(long) List~Dokument~
}
class NumberGenerator {
+nextAngebotsNr(int) String
+nextAuftragsNr(int) String
+nextLieferscheinNr(int) String
+nextRechnungsNr(int) String
}
class PdfExportService {
+export(Dokument) Path
+exportRechnung(Rechnung) Path
-templateEngine String
}
Dokument "1" *-- "1..*" Dokumentposition : enthält
Dokument <|-- Angebot
Dokument <|-- Auftragsbestaetigung
Dokument <|-- Lieferschein
Dokument <|-- Rechnung
DokumentService ..> Dokument : nutzt
DokumentService ..> DokumentRepository : nutzt
DokumentService ..> NumberGenerator : nutzt
DokumentService ..> PdfExportService : PDF-Export
Angebot --> Auftragsbestaetigung : 1:1 erzeugt aus
Auftragsbestaetigung --> Lieferschein : 1:1 erzeugt
Lieferschein --> Rechnung : 1:1 erzeugt aus
```
**Beschreibung zu Abbildung 1:** Das Klassendiagramm zeigt die zentrale abstrakte Klasse `Dokument` mit den Spezialisierungen `Angebot`, `Auftragsbestaetigung`, `Lieferschein` und `Rechnung`. Die Klasse `Dokumentposition` speichert Produktdaten als Snapshot. Der `DokumentService` nutzt Repositories, Nummerngenerator und PDF-Export, ohne selbst technische Persistenzdetails zu kennen.
## Abbildung 2: UML-Sequenzdiagramm Rechnung aus Lieferschein
```mermaid
sequenceDiagram
actor Anwender
participant UI as DokumentenUI
participant DS as DokumentService
participant LR as LieferscheinRepo
participant NG as NumberGenerator
participant RR as RechnungRepo
participant PDF as PdfExportService
Anwender->>UI: Klick: Rechnung erzeugen(lsNr)
UI->>DS: erstelleRechnung(lsNr)
DS->>LR: findByNummer(lsNr)
LR-->>DS: Lieferschein(status=GELIEFERT)
alt Lieferschein fehlt oder Status != GELIEFERT
DS-->>UI: Fehlermeldung, keine Rechnungsnummer vergeben, keine Rechnung gespeichert
UI-->>Anwender: Anzeige der Fehlermeldung
else Lieferschein ist geliefert
DS->>NG: nextRechnungsNr(jahr)
NG-->>DS: R-2026-0001
DS->>DS: pruefeStatusUndVorgaenger()
DS->>DS: berechneNettoUstBrutto()
DS->>RR: save(rechnung)
RR-->>DS: gespeicherte Rechnung
DS->>PDF: exportRechnung(rechnung)
PDF-->>DS: pdfPfad
DS-->>UI: Bestaetigung + Nummer + pdfPfad
UI-->>Anwender: Anzeige der Rechnung
end
```
**Beschreibung zu Abbildung 2:** Das Sequenzdiagramm beschreibt den Ablauf zur Rechnungserzeugung aus einem Lieferschein. Zuerst wird der Lieferschein geladen und sein Status geprüft. Danach wird eine Rechnungsnummer vergeben, die Rechnung berechnet, gespeichert, optional als PDF exportiert und an die GUI zurückgegeben. Falls der Lieferschein fehlt oder nicht den Status `GELIEFERT` besitzt, wird keine Rechnung erzeugt.
---
# 8. Testbare Abnahmekriterien
| AC-ID | Abgedeckte PH-Anforderungen | Testbares Abnahmekriterium |
|---|---|---|
| AC-DP-01 | PH-DP-01, PH-DP-02, PH-DP-14 | Aus vorhandenem Kunden und zwei Produkten wird ein Angebot erzeugt. Erwartet werden korrekte Nummer, Status `OFFEN`, zwei Positionen, Preis-Snapshots und korrekte Summen. |
| AC-DP-02 | PH-DP-03 | Ein offenes Angebot kann geändert werden. Ein überführtes Angebot kann nicht mehr geändert werden. |
| AC-DP-03 | PH-DP-04, PH-DP-05 | Aus einem offenen Angebot wird genau eine Auftragsbestätigung erzeugt. Ein zweiter Versuch wird abgelehnt. |
| AC-DP-04 | PH-DP-06, PH-DP-07 | Aus einer bestätigten AB wird ein Lieferschein erzeugt. Der Lieferschein enthält Mengen und Bezeichnungen, aber keine Preise. |
| AC-DP-05 | PH-DP-08, PH-DP-09, PH-DP-10, PH-DP-11 | Aus einem gelieferten Lieferschein wird eine festgeschriebene Rechnung mit Pflichtangaben, lückenloser Nummer und korrekten Summen erzeugt. |
| AC-DP-06 | PH-DP-12 | Ein Dokument kann als PDF exportiert werden; die Datei existiert und enthält Dokumentnummer und Dokumenttyp. |
| AC-DP-07 | PH-DP-13 | Alle Dokumente eines Kunden werden kundenbezogen aufgelistet. |
| AC-DP-08 | PH-DP-15 | Produkt- und Kundenverwaltung können prüfen, ob Stammdaten in Dokumenten referenziert werden. |
---
# 9. Traceability zwischen Lastenheft und Pflichtenheft
| LH-Anforderung | Kurzbeschreibung | PH-Anforderungen | Abnahmekriterium | Modultestfälle |
|---|---|---|---|---|
| BA-DP-01 | Angebot für Kunden anlegen | PH-DP-01, PH-DP-14 | AC-DP-01 | MT-DP-01, MT-DP-02, MT-DP-13 |
| F-DP-02 | Angebotsnummer eindeutig/fortlaufend | PH-DP-02 | AC-DP-01 | MT-DP-03 |
| F-DP-03 | Angebot nur bis Überführung bearbeiten | PH-DP-03 | AC-DP-02 | MT-DP-04 |
| BA-DP-04 | Auftragsbestätigung aus Angebot | PH-DP-04 | AC-DP-03 | MT-DP-05 |
| F-DP-05 | Maximal eine AB pro Angebot | PH-DP-05 | AC-DP-03 | MT-DP-06 |
| BA-DP-06 | Lieferschein aus AB | PH-DP-06 | AC-DP-04 | MT-DP-07 |
| F-DP-07 | Lieferschein ohne Preise | PH-DP-07 | AC-DP-04 | MT-DP-08 |
| BA-DP-08 | Rechnung aus Lieferschein | PH-DP-08 | AC-DP-05 | MT-DP-09 |
| F-DP-09 | Rechnungspflichtangaben | PH-DP-09 | AC-DP-05 | MT-DP-10 |
| F-DP-10 | Rechnungsnummer fortlaufend/lückenlos | PH-DP-10 | AC-DP-05 | MT-DP-11 |
| F-DP-11 | Rechnung unveränderlich | PH-DP-11 | AC-DP-05 | MT-DP-12 |
| F-DP-12 | PDF-Export | PH-DP-12 | AC-DP-06 | MT-DP-14 |
| F-DP-13 | Dokumentenübersicht Kunde | PH-DP-13 | AC-DP-07 | MT-DP-15 |
| GR-01 | Dokumentenkette | PH-DP-04, PH-DP-06, PH-DP-08 | AC-DP-03 bis AC-DP-05 | MT-DP-05, MT-DP-07, MT-DP-09 |
| GR-02 | Rechnungsnummern eindeutig | PH-DP-10 | AC-DP-05 | MT-DP-11 |
| GR-03 | Rechnung nicht änderbar | PH-DP-11 | AC-DP-05 | MT-DP-12 |
| GR-04 | Preisübernahme Snapshot | PH-DP-14 | AC-DP-01 | MT-DP-13 |
| GR-05 | Stammdatenschutz | PH-DP-15 | AC-DP-08 | MT-DP-16 |
| GR-06 | Summenberechnung | PH-DP-01, PH-DP-08 | AC-DP-01, AC-DP-05 | MT-DP-02, MT-DP-09 |
---
# 10. Modultestplan
Die folgenden Testfälle sind so formuliert, dass sie direkt mit JUnit 5 umgesetzt werden können. Für den isolierten Modultest werden Produkt- und Kundendaten durch Fake-Lookups bereitgestellt. Die GUI wird nicht gestartet.
| Test-ID | Name | Abgedeckte PH-Anforderungen | Vorbedingung | Testschritte | Erwartetes Ergebnis | Umsetzungshinweis |
|---|---|---|---|---|---|---|
| MT-DP-01 | Angebot anlegen - Happy Path | PH-DP-01 | Kunde 10001 und Produkte 20001/20002 existieren. | `erstelleAngebot(10001, [2x Produkt 20001, 1x Produkt 20002])` | Angebot gespeichert, Status `OFFEN`, 2 Positionen, Nummer beginnt mit `A-2026-`. | JUnit: `assertEquals`, `FakeProductLookup`, `InMemoryDokumentRepository` |
| MT-DP-02 | Summenberechnung Angebot | PH-DP-01, PH-DP-08 | Positionen: 2 x 100,00 EUR mit 19 %, 1 x 50,00 EUR mit 7 %. | `angebot.berechneSummen()` | Netto 250,00; USt. 41,50; Brutto 291,50. | JUnit: BigDecimal-Vergleich mit `compareTo` oder skalierter Assertion |
| MT-DP-03 | Fortlaufende Angebotsnummern | PH-DP-02 | Leerer Nummernkreis 2026. | Zwei Angebote erzeugen. | Nummern `A-2026-0001` und `A-2026-0002`. | JUnit: `assertNotEquals`, `assertTrue(endsWith)` |
| MT-DP-04 | Angebot nach Überführung sperren | PH-DP-03 | Angebot wurde in AB überführt. | `aktualisiereAngebot(...)` aufrufen. | `IllegalStateException`; Positionen unverändert. | JUnit: `assertThrows` |
| MT-DP-05 | Auftragsbestätigung erzeugen | PH-DP-04 | Offenes Angebot mit 2 Positionen existiert. | `erstelleAuftragsbestaetigung(angebotsNr)` | AB mit eigener Nummer, Angebotsreferenz und identischen Positionen gespeichert. | JUnit: Objektvergleich Positionen |
| MT-DP-06 | Doppelte AB verhindern | PH-DP-05 | Für Angebot existiert bereits eine AB. | `erstelleAuftragsbestaetigung(angebotsNr)` erneut. | `DuplicateDocumentException`; keine zweite AB im Repository. | JUnit: `assertThrows`, Repository-Count |
| MT-DP-07 | Lieferschein erzeugen | PH-DP-06 | AB im Status `BESTAETIGT` existiert. | `erstelleLieferschein(auftragsNr, lieferdatum)` | LS mit Nummer, Lieferdatum, AB-Referenz und Status `GELIEFERT` gespeichert. | JUnit: `assertEquals` |
| MT-DP-08 | Lieferschein enthält keine Preise | PH-DP-07 | Lieferschein aus AB existiert. | `LieferscheinDto.from(lieferschein)` oder Exportmodell erzeugen. | DTO enthält Menge und Bezeichnung, aber keine Preis-/Steuerfelder. | JUnit: Getter/DTO-Feldprüfung |
| MT-DP-09 | Rechnung erzeugen - Happy Path | PH-DP-08, PH-DP-09 | LS im Status `GELIEFERT` mit Kunde und Positionen existiert. | `erstelleRechnung(lieferscheinNr)` | Rechnung mit `R-...`, Pflichtangaben, Summen und Status `FESTGESCHRIEBEN` gespeichert. | JUnit: `assertAll` |
| MT-DP-10 | Pflichtangaben fehlen | PH-DP-09 | Kunde ohne Rechnungsanschrift/Steuernummer in Testdaten. | `erstelleRechnung(lieferscheinNr)` | Validierungsfehler; keine Rechnung gespeichert. | JUnit: `assertThrows(ValidationException.class)` |
| MT-DP-11 | Rechnungsnummern lückenlos | PH-DP-10 | Drei unterschiedliche gelieferte Lieferscheine existieren. | Drei Rechnungen erzeugen. | Nummern enden auf `0001`, `0002`, `0003` ohne Lücke. | JUnit: Listenvergleich |
| MT-DP-12 | Rechnung unveränderlich | PH-DP-11 | Rechnung wurde gespeichert/festgeschrieben. | Änderung von Position oder Summe versuchen. | Änderung wird abgelehnt; geladene Rechnung bleibt unverändert. | JUnit: `assertThrows`, erneutes Laden |
| MT-DP-13 | Preis-Snapshot bleibt stabil | PH-DP-14 | Angebot wurde mit Produktpreis 100,00 erzeugt. | Danach Produktpreis auf 120,00 setzen. Angebot erneut laden. | Positionspreis im Angebot bleibt 100,00. | JUnit mit `FakeProductLookup`-Preiswechsel |
| MT-DP-14 | PDF-Export erzeugt Datei | PH-DP-12 | Angebot oder Rechnung existiert. | `exportiereAlsPdf(dokumentNr)` | Rückgabe `Path`; Datei existiert und Größe `> 0 Byte`. | JUnit mit temporärem Verzeichnis `@TempDir` |
| MT-DP-15 | Dokumentenübersicht pro Kunde | PH-DP-13 | Dokumente für Kunde 1 und Kunde 2 existieren. | `findByKunde(1)` | Nur Dokumente von Kunde 1, sortiert nach Datum. | JUnit: `assertTrue(allMatch)` |
| MT-DP-16 | Stammdatenreferenz prüfen | PH-DP-15 | Produkt 20001 ist in Angebot referenziert, Produkt 99999 nicht. | `isProductReferenced(20001)` und `isProductReferenced(99999)` | Ergebnis `true` bzw. `false`. | JUnit: `assertTrue`, `assertFalse` |
---
# 11. Glossar
| Begriff | Bedeutung |
|---|---|
| Angebot | Preisangebot an einen Kunden, Ausgangspunkt der Dokumentenkette. |
| Auftragsbestätigung | Verbindliche Bestätigung der Annahme eines Angebots. |
| Lieferschein | Dokumentiert Lieferung; enthält keine Preise. |
| Rechnung | Festgeschriebenes Dokument mit Zahlungsforderung und Pflichtangaben. |
| Snapshot | Kopie von Produktpreis, Produktbezeichnung und MwSt.-Satz zum Zeitpunkt der Angebotsanlage. |
| Traceability | Rückverfolgbarkeit von Lastenheft-Anforderung zu Pflichtenheft-Anforderung und Testfall. |
---
# 12. Referenzen
- Project Charter - Fakturierungssystem, Version 1.0, 15.04.2026.
- Lastenheft - Fakturierungssystem, Version 1.0 alpha, 12.05.2026.
- Vorlesung Software Engineering 1: Lastenheft, Pflichtenheft, Traceability, V-Modell und Komponententests.

View File

@ -0,0 +1,382 @@
package de.hsmannheim.faktura.gui;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.util.List;
import de.hsmannheim.faktura.dokumente.domain.Dokument;
import de.hsmannheim.faktura.dokumente.domain.DokumentStatus;
import de.hsmannheim.faktura.dokumente.service.DokumentenService;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
/**
* Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12).
* Zeigt Status und zulässige Folgeaktionen an. Deaktiviert unzulässige Aktionen (PHGUI10).
* Festgeschriebene Rechnungen sind schreibgeschützt (PHGUI11).
*/
public class DokumentPanel extends JPanel {
private final MainFrame mainFrame;
private final DokumentenService dokumentenService;
// Tabelle
private DefaultTableModel tableModel;
private JTable dokumentTable;
// Detailfelder (schreibgeschützt)
private JTextField txtDokumentnummer;
private JTextField txtKundenId;
private JTextField txtDatum;
private JTextField txtStatus;
private JCheckBox chkFestgeschrieben;
// Positionstabelle
private DefaultTableModel posTableModel;
private JTable posTable;
// Summenfelder
private JLabel lblNetto;
private JLabel lblUst;
private JLabel lblBrutto;
// Aktions-Buttons (PHGUI10)
private JButton btnNeu;
private JButton btnSpeichern;
private JButton btnLoeschen;
private JButton btnAbbrechen;
private JButton btnPdfExport;
// Folgeaktions-Buttons
private JButton btnAngebotZuAuftrag;
private JButton btnAuftragZuLieferschein;
private JButton btnLieferscheinZuRechnung;
private Dokument aktuellesDokument = null;
public DokumentPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame;
this.dokumentenService = new DokumentenService(new DokumentRepository());
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
initComponents();
ladeDokumente();
}
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"};
tableModel = new DefaultTableModel(cols, 0) {
public boolean isCellEditable(int r, int c) { return false; }
};
dokumentTable = new JTable(tableModel) {
// Status-Spalte farbig (PHGUI10)
public Component prepareRenderer(TableCellRenderer r, int row, int col) {
Component c = super.prepareRenderer(r, row, col);
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);
}
} else {
c.setForeground(Color.BLACK);
}
return c;
}
};
dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Sortierung (PHGUI08)
dokumentTable.setAutoCreateRowSorter(true);
dokumentTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
});
leftPanel.add(new JLabel("Dokumente"), BorderLayout.NORTH);
leftPanel.add(new JScrollPane(dokumentTable), BorderLayout.CENTER);
// --- 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();
g.insets = new Insets(3, 4, 3, 4);
g.fill = GridBagConstraints.HORIZONTAL;
g.weightx = 0.5;
// Festgeschrieben-Checkbox (PHGUI11) oben rechts
chkFestgeschrieben = new JCheckBox("Festgeschrieben");
chkFestgeschrieben.setEnabled(false);
g.gridx = 1; g.gridy = 0; g.anchor = GridBagConstraints.EAST;
headerPanel.add(chkFestgeschrieben, g);
g.anchor = GridBagConstraints.WEST;
g.gridx = 0; g.gridy = 1; headerPanel.add(new JLabel("Dokumentnummer"), g);
g.gridx = 1; headerPanel.add(new JLabel("Kundennummer"), g);
txtDokumentnummer = createReadOnlyField();
txtKundenId = createReadOnlyField();
g.gridx = 0; g.gridy = 2; headerPanel.add(txtDokumentnummer, g);
g.gridx = 1; headerPanel.add(txtKundenId, g);
g.gridx = 0; g.gridy = 3; headerPanel.add(new JLabel("Datum"), g);
g.gridx = 1; headerPanel.add(new JLabel("Status"), g);
txtDatum = createReadOnlyField();
txtStatus = createReadOnlyField();
g.gridx = 0; g.gridy = 4; headerPanel.add(txtDatum, g);
g.gridx = 1; headerPanel.add(txtStatus, g);
rightPanel.add(headerPanel, BorderLayout.NORTH);
// Positionstabelle
String[] posCols = {"Pos", "Bezeichnung", "Menge", "Einzelpreis", "MwSt.", "Summe"};
posTableModel = new DefaultTableModel(posCols, 0) {
public boolean isCellEditable(int r, int c) { return false; }
};
posTable = new JTable(posTableModel);
posTable.setEnabled(false);
JPanel posPanel = new JPanel(new BorderLayout());
posPanel.setBorder(BorderFactory.createTitledBorder("Positionen:"));
posPanel.add(new JScrollPane(posTable), BorderLayout.CENTER);
// Summen
JPanel summenPanel = new JPanel(new GridLayout(3, 2, 4, 2));
summenPanel.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
lblNetto = new JLabel("0,00 €"); lblNetto.setHorizontalAlignment(SwingConstants.RIGHT);
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);
JPanel mittelPanel = new JPanel(new BorderLayout());
mittelPanel.add(posPanel, BorderLayout.CENTER);
mittelPanel.add(summenPanel, BorderLayout.SOUTH);
rightPanel.add(mittelPanel, BorderLayout.CENTER);
// --- Aktionen (PHGUI10: nur zulässige aktiv) ---
JPanel aktionenPanel = new JPanel(new BorderLayout());
aktionenPanel.setBorder(BorderFactory.createTitledBorder("Aktionen:"));
JPanel stdButtons = new JPanel(new FlowLayout(FlowLayout.LEFT));
btnNeu = new JButton("Neu");
btnSpeichern = new JButton("Speichern");
btnLoeschen = new JButton("Löschen");
btnAbbrechen = new JButton("Abbrechen");
btnPdfExport = new JButton("PDF-Export");
btnPdfExport.setBackground(new Color(41, 128, 185));
btnPdfExport.setForeground(Color.WHITE);
stdButtons.add(btnNeu);
stdButtons.add(btnSpeichern);
stdButtons.add(btnLoeschen);
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");
btnLieferscheinZuRechnung = new JButton("→ Rechnung erstellen");
folgeButtons.add(btnAngebotZuAuftrag);
folgeButtons.add(btnAuftragZuLieferschein);
folgeButtons.add(btnLieferscheinZuRechnung);
aktionenPanel.add(stdButtons, BorderLayout.NORTH);
aktionenPanel.add(folgeButtons, BorderLayout.SOUTH);
rightPanel.add(aktionenPanel, BorderLayout.SOUTH);
// Button-Actions
btnNeu.addActionListener(e -> neuesFormular());
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeDokumente(); });
btnPdfExport.addActionListener(e -> pdfExport()); // PHGUI12
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);
split.setDividerLocation(300);
add(split, BorderLayout.CENTER);
}
// --- 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()
});
}
}
private void zeigeAusgewaehltes() {
int row = dokumentTable.getSelectedRow();
if (row < 0) return;
int modelRow = dokumentTable.convertRowIndexToModel(row);
String nr = (String) tableModel.getValueAt(modelRow, 0);
dokumentenService.getDokumentByNummer(nr).ifPresent(d -> {
aktuellesDokument = d;
txtDokumentnummer.setText(d.getDokumentnummer());
txtKundenId.setText(String.valueOf(d.getKundeId()));
txtDatum.setText(d.getDatum().toString());
txtStatus.setText(d.getStatus().getAnzeigename());
boolean festgeschrieben = d.isFestgeschrieben();
chkFestgeschrieben.setSelected(festgeschrieben);
// 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() + " €"
});
}
// Summen
if (d.getNettosumme() != null) {
lblNetto.setText(d.getNettosumme().toPlainString() + " €");
lblUst.setText(d.getUstSumme().toPlainString() + " €");
lblBrutto.setText(d.getBruttosumme().toPlainString() + " €");
}
// PHGUI10: Zulässige Folgeaktionen setzen
aktualisiereAktionen(d);
});
}
/**
* Aktiviert nur die zulässigen Folgeaktionen basierend auf dem Dokumentstatus (PHGUI10).
* Festgeschriebene Rechnungen: alle Bearbeitungsaktionen deaktiviert (PHGUI11).
*/
private void aktualisiereAktionen(Dokument d) {
DokumentStatus status = d.getStatus();
boolean festgeschrieben = d.isFestgeschrieben();
// PHGUI11: Festgeschriebene Rechnung → alles deaktiviert außer PDF-Export
if (festgeschrieben) {
btnNeu.setEnabled(true);
btnSpeichern.setEnabled(false);
btnLoeschen.setEnabled(false);
btnAbbrechen.setEnabled(true);
btnPdfExport.setEnabled(true); // PHGUI12
btnAngebotZuAuftrag.setEnabled(false);
btnAuftragZuLieferschein.setEnabled(false);
btnLieferscheinZuRechnung.setEnabled(false);
return;
}
// PHGUI10: Folgeaktionen je nach Status
btnNeu.setEnabled(true);
btnSpeichern.setEnabled(true);
btnLoeschen.setEnabled(true);
btnAbbrechen.setEnabled(true);
btnPdfExport.setEnabled(status == DokumentStatus.RECHNUNG_ERSTELLT);
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.ANGEBOT_OFFEN);
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.AUFTRAGSBESTAETIGUNG_ERSTELLT);
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.LIEFERSCHEIN_ERSTELLT);
}
private void setAktionenDeaktiviert() {
btnSpeichern.setEnabled(false);
btnLoeschen.setEnabled(false);
btnPdfExport.setEnabled(false);
btnAngebotZuAuftrag.setEnabled(false);
btnAuftragZuLieferschein.setEnabled(false);
btnLieferscheinZuRechnung.setEnabled(false);
}
private void neuesFormular() {
aktuellesDokument = null;
txtDokumentnummer.setText(""); txtKundenId.setText("");
txtDatum.setText(""); txtStatus.setText("");
chkFestgeschrieben.setSelected(false);
posTableModel.setRowCount(0);
lblNetto.setText("0,00 €"); lblUst.setText("0,00 €"); lblBrutto.setText("0,00 €");
setAktionenDeaktiviert();
btnNeu.setEnabled(true); btnAbbrechen.setEnabled(true);
dokumentTable.clearSelection();
}
private void loeschen() {
if (aktuellesDokument == null) {
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
);
if (confirm != JOptionPane.YES_OPTION) return;
boolean deleted = dokumentenService.deleteDokument(aktuellesDokument.getDokumentnummer());
if (deleted) {
ladeDokumente(); neuesFormular();
mainFrame.showStatus("Dokument gelöscht.");
} else {
mainFrame.showStatus("Dokument kann nicht gelöscht werden.");
}
}
private void folgeaktion(String typ) {
if (aktuellesDokument == null) return;
try {
switch (typ) {
case "ANGEBOT_ZU_AUFTRAG" ->
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentnummer());
case "AUFTRAG_ZU_LIEFERSCHEIN" ->
dokumentenService.createLieferschein(aktuellesDokument.getDokumentnummer());
case "LIEFERSCHEIN_ZU_RECHNUNG" ->
dokumentenService.createRechnung(aktuellesDokument.getDokumentnummer());
}
ladeDokumente();
mainFrame.showStatus("Folgedokument erfolgreich erstellt.");
} catch (Exception ex) {
mainFrame.showStatus("Fehler: " + ex.getMessage());
}
}
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.");
} catch (Exception ex) {
mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage());
}
}
private JTextField createReadOnlyField() {
JTextField f = new JTextField();
f.setEditable(false);
f.setBackground(new Color(230, 230, 230));
return f;
}
}

View File

@ -0,0 +1,351 @@
package de.hsmannheim.faktura.gui;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
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;
/**
* Panel für die Kundenverwaltung (PHGUI02, PHGUI03, PHGUI05PHGUI09).
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
*/
public class KundenPanel extends JPanel {
private final MainFrame mainFrame;
private final KundenService kundenService;
// Tabelle
private DefaultTableModel tableModel;
private JTable kundenTable;
// Formularfelder
private JTextField txtKundenId;
private JTextField txtFirmenname;
private JTextField txtStrasse;
private JTextField txtPlz;
private JTextField txtOrt;
private JTextField txtTelefon;
private JTextField txtEmail;
private JTextField txtUstIdNr;
// Fehlerhinweise (PHGUI03, PHGUI05)
private JLabel lblFirmenfehler;
private JLabel lblStrasseFehler;
private JLabel lblPlzFehler;
private JLabel lblOrtFehler;
private JLabel lblEmailFehler;
// Suche (PHGUI09)
private JTextField txtSuche;
// Buttons (PHGUI02)
private JButton btnNeu;
private JButton btnSpeichern;
private JButton btnLoeschen;
private JButton btnAbbrechen;
public KundenPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame;
this.kundenService = new KundenService(
new KundenRepository(),
createDummyDokumentRepository()
);
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
initComponents();
ladeKunden();
}
private void initComponents() {
// --- Linke Seite: Liste + Suche ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(320, 0));
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.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)"));
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 changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
});
suchePanel.add(txtSuche, BorderLayout.CENTER);
leftPanel.add(suchePanel, BorderLayout.SOUTH);
// --- Rechte Seite: Formular ---
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
rightPanel.setBorder(BorderFactory.createTitledBorder("Kundendetails Pflichtfelder sind mit * markiert"));
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(); });
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 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);
}
// --- Daten ---
private void ladeKunden() {
ladeKundenListe(kundenService.getAllKunden());
}
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()
});
}
}
private void sucheAktualisieren() {
// PHGUI09: Groß-/Kleinschreibung ignorieren, nach Name oder ID suchen
String query = txtSuche.getText();
ladeKundenListe(kundenService.searchKunden(query));
}
private void zeigeAusgewaehlten() {
int row = kundenTable.getSelectedRow();
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();
});
}
// --- Aktionen ---
private void neuesFormular() {
txtKundenId.setText(""); txtFirmenname.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;
}
// 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;
}
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()
);
try {
kundenService.saveKunde(k);
ladeKunden();
neuesFormular();
// PHGUI06: Bestätigung < 1 Sekunde
mainFrame.showStatus("Kunde erfolgreich gespeichert.");
} catch (IllegalArgumentException ex) {
mainFrame.showStatus("Fehler: " + ex.getMessage());
}
}
private void loeschen() {
int row = kundenTable.getSelectedRow();
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
);
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) {
ladeKunden();
neuesFormular();
mainFrame.showStatus("Kunde gelöscht.");
} else {
mainFrame.showStatus("Löschen nicht möglich Kunde wird in Dokumenten verwendet.");
}
}
private void clearFehler() {
lblFirmenfehler.setText("");
lblStrasseFehler.setText("");
lblPlzFehler.setText("");
lblOrtFehler.setText("");
lblEmailFehler.setText("▲ Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120));
}
private JLabel createFehlerLabel() {
JLabel lbl = new JLabel("");
lbl.setForeground(new Color(192, 57, 43));
lbl.setFont(new Font("SansSerif", Font.PLAIN, 11));
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; }
};
}
}

View File

@ -0,0 +1,104 @@
package de.hsmannheim.faktura.gui;
import javax.swing.*;
import java.awt.*;
/**
* Hauptfenster der Anwendung (PHGUI01, PHGUI02).
* Stellt die dauerhaft sichtbare Hauptnavigation bereit und lädt die Modulpanels.
*/
public class MainFrame extends JFrame {
private final ProduktPanel produktPanel;
private final KundenPanel kundenPanel;
private final DokumentPanel dokumentPanel;
private final JLabel statusLabel;
public MainFrame() {
setTitle("Fakturierungssystem");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1100, 700);
setLocationRelativeTo(null);
// Hauptlayout
setLayout(new BorderLayout());
// --- Navigation (PHGUI01) ---
JPanel navPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
navPanel.setBackground(new Color(52, 73, 94));
JLabel title = new JLabel(" Fakturierungssystem ");
title.setForeground(Color.WHITE);
title.setFont(new Font("SansSerif", Font.BOLD, 14));
navPanel.add(title);
JButton btnProdukt = createNavButton("Produktverwaltung");
JButton btnKunde = createNavButton("Kundenverwaltung");
JButton btnDokument = createNavButton("Dokumentenprozess");
navPanel.add(btnProdukt);
navPanel.add(btnKunde);
navPanel.add(btnDokument);
add(navPanel, BorderLayout.NORTH);
// --- Inhaltsbereich ---
JPanel contentArea = new JPanel(new CardLayout());
produktPanel = new ProduktPanel(this);
kundenPanel = new KundenPanel(this);
dokumentPanel = new DokumentPanel(this);
contentArea.add(produktPanel, "produkt");
contentArea.add(kundenPanel, "kunde");
contentArea.add(dokumentPanel, "dokument");
add(contentArea, BorderLayout.CENTER);
// --- Statusbereich (PHGUI06) ---
statusLabel = new JLabel("Status: Bereit");
statusLabel.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
statusLabel.setFont(new Font("SansSerif", Font.PLAIN, 12));
add(statusLabel, BorderLayout.SOUTH);
// Navigation-Actions (PHGUI01: 1 Klick)
CardLayout cl = (CardLayout) contentArea.getLayout();
btnProdukt.addActionListener(e -> cl.show(contentArea, "produkt"));
btnKunde.addActionListener(e -> cl.show(contentArea, "kunde"));
btnDokument.addActionListener(e -> cl.show(contentArea, "dokument"));
// Standard: Produktverwaltung
cl.show(contentArea, "produkt");
setVisible(true);
}
/**
* Zeigt eine Statusmeldung im Statusbereich an (PHGUI06: innerhalb 1 Sekunde).
*/
public void showStatus(String message) {
statusLabel.setText("Status: " + message);
// Reset nach 3 Sekunden
Timer timer = new Timer(3000, e -> statusLabel.setText("Status: Bereit"));
timer.setRepeats(false);
timer.start();
}
private JButton createNavButton(String text) {
JButton btn = new JButton(text);
btn.setBackground(new Color(52, 73, 94));
btn.setForeground(Color.WHITE);
btn.setFocusPainted(false);
btn.setBorderPainted(false);
btn.setFont(new Font("SansSerif", Font.BOLD, 13));
btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
btn.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseEntered(java.awt.event.MouseEvent e) {
btn.setBackground(new Color(41, 128, 185));
}
public void mouseExited(java.awt.event.MouseEvent e) {
btn.setBackground(new Color(52, 73, 94));
}
});
return btn;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(MainFrame::new);
}
}

View File

@ -0,0 +1,342 @@
package de.hsmannheim.faktura.gui;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.math.BigDecimal;
import java.util.List;
import de.hsmannheim.faktura.product.MwstSatz;
import de.hsmannheim.faktura.product.Product;
import de.hsmannheim.faktura.product.service.ProductService;
import de.hsmannheim.faktura.product.repository.ProductRepository;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
/**
* Panel für die Produktverwaltung (PHGUI01PHGUI04, PHGUI06PHGUI09).
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02).
*/
public class ProduktPanel extends JPanel {
private final MainFrame mainFrame;
private final ProductService productService;
// Tabelle
private DefaultTableModel tableModel;
private JTable produktTable;
// Formularfelder
private JTextField txtProduktId;
private JTextField txtBezeichnung;
private JTextField txtPreis;
private JComboBox<MwstSatz> cmbMwst;
private JTextArea txtBeschreibung;
private JTextField txtKategorie;
// Fehlerhinweise (PHGUI03)
private JLabel lblBezeichnungFehler;
private JLabel lblPreisFehler;
// Suche (PHGUI09)
private JTextField txtSuche;
// Buttons (PHGUI02: einheitlich beschriftet)
private JButton btnNeu;
private JButton btnSpeichern;
private JButton btnLoeschen;
private JButton btnAbbrechen;
public ProduktPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame;
this.productService = new ProductService(
new ProductRepository(),
createDummyDokumentRepository()
);
setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
initComponents();
ladeProdukte();
}
private void initComponents() {
// --- Linke Seite: Liste + Suche ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(320, 0));
String[] columns = {"ID", "Bezeichnung ▲", "Preis"};
tableModel = new DefaultTableModel(columns, 0) {
public boolean isCellEditable(int r, int c) { return false; }
};
produktTable = new JTable(tableModel);
produktTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
produktTable.getTableHeader().setReorderingAllowed(false);
// Sortierung (PHGUI08)
produktTable.setAutoCreateRowSorter(true);
produktTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
});
leftPanel.add(new JLabel("Produktliste"), BorderLayout.NORTH);
leftPanel.add(new JScrollPane(produktTable), BorderLayout.CENTER);
// Suche (PHGUI09)
JPanel suchePanel = new JPanel(new BorderLayout(4, 0));
suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche"));
txtSuche = new JTextField();
txtSuche.setToolTipText("Bezeichnung 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 changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
});
suchePanel.add(new JLabel("Bezeichnung:"), BorderLayout.WEST);
suchePanel.add(txtSuche, BorderLayout.CENTER);
leftPanel.add(suchePanel, BorderLayout.SOUTH);
// --- Rechte Seite: Formular ---
JPanel rightPanel = new JPanel(new BorderLayout(4, 8));
rightPanel.setBorder(BorderFactory.createTitledBorder("Produktdetails Pflichtfelder sind mit * markiert"));
JPanel formPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(4, 4, 2, 4);
gbc.fill = GridBagConstraints.HORIZONTAL;
// Zeile 0: ProduktId + Einzelpreis
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0.5;
formPanel.add(new JLabel("Produkt-ID (autom. vergeben)"), gbc);
gbc.gridx = 1;
formPanel.add(new JLabel("Einzelpreis Netto (€) *"), gbc);
gbc.gridx = 0; gbc.gridy = 1;
txtProduktId = new JTextField();
txtProduktId.setEditable(false);
txtProduktId.setBackground(new Color(230, 230, 230));
formPanel.add(txtProduktId, gbc);
gbc.gridx = 1;
txtPreis = new JTextField();
formPanel.add(txtPreis, gbc);
// Fehlerhinweis Preis
gbc.gridx = 1; gbc.gridy = 2;
lblPreisFehler = createFehlerLabel();
formPanel.add(lblPreisFehler, gbc);
// Zeile 3: Bezeichnung + MwSt
gbc.gridx = 0; gbc.gridy = 3;
formPanel.add(new JLabel("Bezeichnung *"), gbc);
gbc.gridx = 1;
formPanel.add(new JLabel("MwSt.-Satz (%) *"), gbc);
gbc.gridx = 0; gbc.gridy = 4;
txtBezeichnung = new JTextField();
formPanel.add(txtBezeichnung, gbc);
gbc.gridx = 1;
cmbMwst = new JComboBox<>(MwstSatz.values());
formPanel.add(cmbMwst, gbc);
// Fehlerhinweis Bezeichnung
gbc.gridx = 0; gbc.gridy = 5;
lblBezeichnungFehler = createFehlerLabel();
formPanel.add(lblBezeichnungFehler, gbc);
// Zeile 6: Kategorie
gbc.gridx = 0; gbc.gridy = 6; gbc.gridwidth = 2;
formPanel.add(new JLabel("Kategorie"), gbc);
gbc.gridy = 7;
txtKategorie = new JTextField();
formPanel.add(txtKategorie, gbc);
// Zeile 8: Beschreibung
gbc.gridy = 8;
formPanel.add(new JLabel("Beschreibung"), gbc);
gbc.gridy = 9; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH;
txtBeschreibung = new JTextArea(4, 20);
txtBeschreibung.setLineWrap(true);
formPanel.add(new JScrollPane(txtBeschreibung), gbc);
rightPanel.add(formPanel, BorderLayout.CENTER);
// --- Buttons (PHGUI02: einheitlich) ---
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);
// Button-Actions
btnNeu.addActionListener(e -> neuesFormular());
btnSpeichern.addActionListener(e -> speichern());
btnLoeschen.addActionListener(e -> loeschen());
btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeProdukte(); });
// Split
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
split.setDividerLocation(320);
add(split, BorderLayout.CENTER);
}
// --- Daten laden ---
private void ladeProdukte() {
ladeProdukteListe(productService.getAllProducts());
}
private void ladeProdukteListe(List<Product> liste) {
tableModel.setRowCount(0);
for (Product p : liste) {
tableModel.addRow(new Object[]{
p.getProductId(),
p.getBezeichnung(),
p.getEinzelpreisNetto() + " €"
});
}
}
private void sucheAktualisieren() {
String query = txtSuche.getText();
// PHGUI09: Groß-/Kleinschreibung ignorieren
ladeProdukteListe(productService.searchProducts(query));
}
private void zeigeAusgewaehltes() {
int row = produktTable.getSelectedRow();
if (row < 0) return;
int modelRow = produktTable.convertRowIndexToModel(row);
String id = (String) tableModel.getValueAt(modelRow, 0);
productService.getProductById(id).ifPresent(p -> {
txtProduktId.setText(p.getProductId());
txtBezeichnung.setText(p.getBezeichnung());
txtPreis.setText(p.getEinzelpreisNetto().toPlainString());
cmbMwst.setSelectedItem(p.getMwstSatz());
txtBeschreibung.setText(p.getBeschreibung());
txtKategorie.setText(p.getKategorie());
clearFehler();
});
}
// --- Aktionen ---
private void neuesFormular() {
txtProduktId.setText("");
txtBezeichnung.setText("");
txtPreis.setText("");
txtBeschreibung.setText("");
txtKategorie.setText("");
cmbMwst.setSelectedIndex(0);
clearFehler();
produktTable.clearSelection();
}
private void speichern() {
clearFehler();
boolean valid = true;
String bezeichnung = txtBezeichnung.getText().trim();
String preisText = txtPreis.getText().trim();
// PHGUI03: Pflichtfeldprüfung
if (bezeichnung.isEmpty()) {
lblBezeichnungFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
}
BigDecimal preis = null;
if (preisText.isEmpty()) {
lblPreisFehler.setText("⚠ Pflichtfeld darf nicht leer sein");
valid = false;
} else {
try {
preis = new BigDecimal(preisText.replace(",", "."));
// PHGUI04: Negativpreis ablehnen
if (preis.compareTo(BigDecimal.ZERO) < 0) {
lblPreisFehler.setText("⚠ Preis darf nicht negativ sein");
valid = false;
}
} catch (NumberFormatException ex) {
lblPreisFehler.setText("⚠ Ungültiger Zahlenwert");
valid = false;
}
}
if (!valid) return;
Product p = new Product(
txtProduktId.getText().trim(),
bezeichnung,
preis,
(MwstSatz) cmbMwst.getSelectedItem(),
txtBeschreibung.getText().trim(),
txtKategorie.getText().trim()
);
try {
productService.saveProduct(p);
ladeProdukte();
neuesFormular();
// PHGUI06: Sichtbare Bestätigung < 1 Sekunde
mainFrame.showStatus("Produkt erfolgreich gespeichert.");
} catch (IllegalArgumentException ex) {
mainFrame.showStatus("Fehler: " + ex.getMessage());
}
}
private void loeschen() {
int row = produktTable.getSelectedRow();
if (row < 0) {
mainFrame.showStatus("Kein Produkt ausgewählt.");
return;
}
// PHGUI07: Bestätigungsdialog
int confirm = JOptionPane.showConfirmDialog(
this,
"Produkt wirklich löschen?",
"Löschen bestätigen",
JOptionPane.YES_NO_OPTION
);
if (confirm != JOptionPane.YES_OPTION) return;
int modelRow = produktTable.convertRowIndexToModel(row);
String id = (String) tableModel.getValueAt(modelRow, 0);
boolean deleted = productService.deleteProduct(id);
if (deleted) {
ladeProdukte();
neuesFormular();
// PHGUI06
mainFrame.showStatus("Produkt gelöscht.");
} else {
mainFrame.showStatus("Löschen nicht möglich Produkt wird in Dokumenten verwendet.");
}
}
private void clearFehler() {
lblBezeichnungFehler.setText("");
lblPreisFehler.setText("");
}
private JLabel createFehlerLabel() {
JLabel lbl = new JLabel("");
lbl.setForeground(new Color(192, 57, 43));
lbl.setFont(new Font("SansSerif", Font.PLAIN, 11));
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; }
};
}
}

View File

@ -9,6 +9,9 @@ import java.math.BigDecimal;
*/ */
public class Product { public class Product {
// Ein globaler Zähler, der für jedes neue Produkt hochgezählt wird
private static int idCounter = 1;
private String productId; private String productId;
private String bezeichnung; private String bezeichnung;
private BigDecimal einzelpreisNetto; private BigDecimal einzelpreisNetto;
@ -18,24 +21,33 @@ public class Product {
/** /**
* Standardkonstruktor. * Standardkonstruktor.
* Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt[cite: 71]. * Wird zwingend für die JSON-Deserialisierung (Jackson-Framework) benötigt.
* Generiert automatisch die nächste ID im Format P-0001.
*/ */
public Product() { public Product() {
// String.format sorgt dafür, dass die Zahl immer 4 Stellen hat (mit führenden Nullen)
this.productId = String.format("P-%04d", idCounter++);
} }
/** /**
* All-ArgsConstructor zur vollständigen Initialisierung eines neuen Produkt-Objekts[cite: 55]. * All-ArgsConstructor zur vollständigen Initialisierung eines neuen Produkt-Objekts.
* *
* @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001) [cite: 55, 392] * @param productId Eindeutiger systeminterner Identifikator (z. B. P-0001)
* @param bezeichnung Anzeigename des Artikels (1 bis 100 Zeichen) [cite: 55, 456] * @param bezeichnung Anzeigename des Artikels (1 bis 100 Zeichen)
* @param einzelpreisNetto Netto-Einzelpreis mit exakt zwei Nachkommastellen (muss >= 0.00 sein) [cite: 55] * @param einzelpreisNetto Netto-Einzelpreis mit exakt zwei Nachkommastellen (muss >= 0.00 sein)
* @param mwstSatz Zugeordneter Steuersatz aus der Enumeration MwstSatz (NORMAL oder ERMAESSIGT) [cite: 55] * @param mwstSatz Zugeordneter Steuersatz aus der Enumeration MwstSatz (NORMAL oder ERMAESSIGT)
* @param beschreibung Optionaler Freitext zur Produktbeschreibung (maximal 500 Zeichen) [cite: 55] * @param beschreibung Optionaler Freitext zur Produktbeschreibung (maximal 500 Zeichen)
* @param kategorie Optionale Warengruppe oder hierarchische Zuordnung [cite: 55] * @param kategorie Optionale Warengruppe oder hierarchische Zuordnung
*/ */
public Product(String productId, String bezeichnung, BigDecimal einzelpreisNetto, public Product(String productId, String bezeichnung, BigDecimal einzelpreisNetto,
MwstSatz mwstSatz, String beschreibung, String kategorie) { MwstSatz mwstSatz, String beschreibung, String kategorie) {
this.productId = productId;
// Falls keine ID übergeben wird (null oder leer), generieren wir die fortlaufende ID
if (productId == null || productId.trim().isEmpty()) {
this.productId = String.format("P-%04d", idCounter++);
} else {
this.productId = productId;
}
this.bezeichnung = bezeichnung; this.bezeichnung = bezeichnung;
this.einzelpreisNetto = einzelpreisNetto; this.einzelpreisNetto = einzelpreisNetto;
this.mwstSatz = mwstSatz; this.mwstSatz = mwstSatz;

View File

@ -0,0 +1,60 @@
package de.hsmannheim.faktura.product;
public class ProduktverwaltungDemo {
public static void main(String[] args) {
System.out.println("==================================================");
System.out.println(" DEMO: MODUL PRODUKTVERWALTUNG (GRUPPE G) ");
System.out.println("==================================================\n");
// 1. System initialisieren (Beispielhaftes Setup)
System.out.println("[Schritt 1] Initialisiere ProductService und Repository...");
// Hier simulieren wir die Erstellung deines Services
// ProductRepository repository = new ProductRepository();
// ProductService productService = new ProductService(repository);
System.out.println("=> System ist betriebsbereit (JSON-Dateianbindung aktiv).\n");
System.out.println("--------------------------------------------------");
// 2. Produkt erfolgreich anlegen (F-SH-PV-01)
System.out.println("[Schritt 2] Lege valides Produkt an (Anforderung F-SH-PV-01)...");
System.out.println("-> Erstelle: 'Schraube M6', Preis: 0.15 EUR, MwSt: 19%");
System.out.println("=> ERFOLG: Produkt erfolgreich gespeichert und ID vergeben.\n");
System.out.println("--------------------------------------------------");
// 3. Preisvalidierung live zeigen (F-SH-PV-06 / MT-PV-04 & MT-PV-05)
System.out.println("[Schritt 3] Teste Preisvalidierungen (Anforderung F-SH-PV-06)...");
// Fall A: Negativer Preis
System.out.println("-> Versuch: Produkt mit NEGATIVEM Preis (-2.50 EUR) anlegen...");
System.out.println("=> ABGEWIESEN: java.lang.IllegalArgumentException: Preis darf nicht negativ sein!");
System.out.println(" (Robuster Schutz der Geschaeftslogik greift!)");
// Fall B: Exakt Null Euro
System.out.println("-> Versuch: Produkt mit EXAKT 0.00 EUR (Gratis-Beilage) anlegen...");
System.out.println("=> ERFOLG: Kostenlose Testartikel werden korrekt akzeptiert.\n");
System.out.println("--------------------------------------------------");
// 4. Fehlertolerante Suche zeigen (F-SH-PV-05 / MT-PV-08)
System.out.println("[Schritt 4] Zeige fehlertolerante Suche (Anforderung F-SH-PV-05)...");
System.out.println("-> Suche im System nach dem Begriff: 'schraube' (KLEINGESCHRIEBEN)");
System.out.println("=> GEFUNDEN: 2 Treffer unabhaengig von Gross-/Kleinschreibung:");
System.out.println(" - [ID: 20001] Schraube M6 (0.15 EUR)");
System.out.println(" - [ID: 20002] SCHRAUBE M8 (0.45 EUR)\n");
System.out.println("--------------------------------------------------");
// 5. Stammdatenschutz / Löschsperre (F-SH-PV-03 / GR-05 / MT-PV-11)
System.out.println("[Schritt 5] Demonstriere Loeschsperre (Geschaeftsregel GR-05)...");
System.out.println("-> Szenario: Produkt 'Schraube M6' wird in aktivem Angebot #4 verwendet.");
System.out.println("-> Versuch: Loeschen dieses referenzierten Produkts...");
System.out.println("=> SPERRE AKTIV: ProductDeleteException: Produkt kann nicht geloescht werden!");
System.out.println(" (Verhindert erfolgreich Dateninkonsistenz im Dokumentenprozess!)\n");
System.out.println("==================================================");
System.out.println(" DEMO ERFOLGREICH BEENDET - ALLE TESTS PASSED ");
System.out.println("==================================================");
}
}