main
3027248 2026-06-28 15:55:34 +02:00
parent 28dcdbc855
commit 2483d51cb7
5 changed files with 701 additions and 431 deletions

17
data/kunden.json 100644
View File

@ -0,0 +1,17 @@
{
"kunden" : [ {
"kundeId" : 1,
"firmenname" : "Aura GmBH & Co KG",
"nachname" : "Patric",
"vorname" : "Nachname",
"strasse" : "Muster Straße, 2",
"plz" : "67480",
"ort" : "Edenkoben",
"telefon" : "7265389761",
"email" : "Aura@Email.com",
"ustIdNr" : "28976398",
"lieferadresse" : null,
"ansprechpartner" : null
} ],
"naechsteId" : 2
}

View File

@ -5,4 +5,11 @@
"mwstSatz" : "NORMAL", "mwstSatz" : "NORMAL",
"beschreibung" : "Schlagbohrer", "beschreibung" : "Schlagbohrer",
"kategorie" : "Elektrowerkzeuge" "kategorie" : "Elektrowerkzeuge"
}, {
"productId" : "PROD-dca63cf9",
"bezeichnung" : "Schraubenschlüssel",
"einzelpreisNetto" : 42.33,
"mwstSatz" : "NORMAL",
"beschreibung" : "Test",
"kategorie" : "Werkzeug"
} ] } ]

View File

@ -38,9 +38,9 @@ public class DokumentServiceImpl implements DokumentService {
* Alle Abhängigkeiten werden injiziert (Dependency Injection). * Alle Abhängigkeiten werden injiziert (Dependency Injection).
*/ */
public DokumentServiceImpl(DokumentRepository repository, public DokumentServiceImpl(DokumentRepository repository,
ProductLookup productLookup, ProductLookup productLookup,
CustomerLookup customerLookup, CustomerLookup customerLookup,
PdfExportService pdfExportService) { PdfExportService pdfExportService) {
this.repository = repository; this.repository = repository;
this.productLookup = productLookup; this.productLookup = productLookup;
this.customerLookup = customerLookup; this.customerLookup = customerLookup;
@ -113,18 +113,23 @@ public class DokumentServiceImpl implements DokumentService {
public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) { public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) {
Angebot angebot = ladeAngebot(angebotsNummer); Angebot angebot = ladeAngebot(angebotsNummer);
// Status-Prüfung (PH-DP-04) // Doppelte AB zuerst (PH-DP-05 / MT-DP-06): wenn bereits eine AB für
if (angebot.getStatus() != DokumentStatus.OFFEN) { // dieses Angebot existiert, ist das Angebot zwangsläufig UEBERFUEHRT
throw new IllegalStateException( // und die Status-Prüfung würde ebenfalls anschlagen. Die Spec verlangt
"Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " + // in diesem Szenario eindeutig DuplicateDocumentException, deshalb
angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden."); // muss diese Prüfung vor dem allgemeinen Status-Check stehen.
}
// Doppelte AB verhindern (PH-DP-05)
if (repository.existsFolgedokument(angebotsNummer)) { if (repository.existsFolgedokument(angebotsNummer)) {
throw new DuplicateDocumentException(angebotsNummer); throw new DuplicateDocumentException(angebotsNummer);
} }
// Allgemeine Status-Prüfung (PH-DP-04): blockiert auch andere
// Nicht-OFFEN-Zustände (z. B. ABGELEHNT, falls künftig ergänzt).
if (angebot.getStatus() != DokumentStatus.OFFEN) {
throw new IllegalStateException(
"Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " +
angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden.");
}
// AB erzeugen Positionen 1:1 übernehmen // AB erzeugen Positionen 1:1 übernehmen
Auftragsbestaetigung ab = new Auftragsbestaetigung(); Auftragsbestaetigung ab = new Auftragsbestaetigung();
ab.setDokumentNummer(abGenerator.naechsteNummer()); ab.setDokumentNummer(abGenerator.naechsteNummer());
@ -156,8 +161,8 @@ public class DokumentServiceImpl implements DokumentService {
if (ab.getStatus() != DokumentStatus.BESTAETIGT) { if (ab.getStatus() != DokumentStatus.BESTAETIGT) {
throw new IllegalStateException( throw new IllegalStateException(
"Auftragsbestätigung " + auftragsNummer + " hat nicht den Status BESTAETIGT " + "Auftragsbestätigung " + auftragsNummer + " hat nicht den Status BESTAETIGT " +
"(aktueller Status: " + ab.getStatus() + ")."); "(aktueller Status: " + ab.getStatus() + ").");
} }
// Positionen ohne Preis-Snapshot (PH-DP-07) // Positionen ohne Preis-Snapshot (PH-DP-07)
@ -189,8 +194,8 @@ public class DokumentServiceImpl implements DokumentService {
if (ls.getStatus() != DokumentStatus.GELIEFERT) { if (ls.getStatus() != DokumentStatus.GELIEFERT) {
throw new IllegalStateException( throw new IllegalStateException(
"Lieferschein " + lieferscheinNummer + " hat nicht den Status GELIEFERT " + "Lieferschein " + lieferscheinNummer + " hat nicht den Status GELIEFERT " +
"(aktueller Status: " + ls.getStatus() + ")."); "(aktueller Status: " + ls.getStatus() + ").");
} }
// Kundendaten für Pflichtangaben holen // Kundendaten für Pflichtangaben holen
@ -320,4 +325,4 @@ public class DokumentServiceImpl implements DokumentService {
pa.setLeistungsdatum(lieferdatum); pa.setLeistungsdatum(lieferdatum);
return pa; return pa;
} }
} }

View File

@ -3,33 +3,69 @@ package de.hsmannheim.faktura.gui;
import javax.swing.*; import javax.swing.*;
import javax.swing.table.*; import javax.swing.table.*;
import java.awt.*; import java.awt.*;
import java.io.File;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional;
import de.hsmannheim.faktura.dokumente.domain.Dokument; import de.hsmannheim.faktura.dokumente.domain.Dokument;
import de.hsmannheim.faktura.dokumente.domain.DokumentPosition;
import de.hsmannheim.faktura.dokumente.domain.DokumentStatus; import de.hsmannheim.faktura.dokumente.domain.DokumentStatus;
import de.hsmannheim.faktura.dokumente.service.DokumentenService; import de.hsmannheim.faktura.dokumente.domain.DokumentType;
import de.hsmannheim.faktura.dokumente.export.PdfExportService;
import de.hsmannheim.faktura.dokumente.export.SimplePdfExportService;
import de.hsmannheim.faktura.dokumente.lookup.CustomerDto;
import de.hsmannheim.faktura.dokumente.lookup.CustomerLookup;
import de.hsmannheim.faktura.dokumente.lookup.ProductDto;
import de.hsmannheim.faktura.dokumente.lookup.ProductLookup;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository; import de.hsmannheim.faktura.dokumente.repository.DokumentRepository;
import de.hsmannheim.faktura.dokumente.repository.InMemoryDokumentRepository;
import de.hsmannheim.faktura.dokumente.service.DokumentService;
import de.hsmannheim.faktura.dokumente.service.DokumentServiceImpl;
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.model.Kunde;
/** /**
* Panel für den Dokumentenprozess (PHGUI10, PHGUI11, PHGUI12). * 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). * Verdrahtung gegen die spec-konforme Schnittstelle aus dem
* Pflichtenheft Gruppe F v1.0 (Kap. 6.2):
* - Service: {@link DokumentService} / {@link DokumentServiceImpl}
* - Repository: {@link DokumentRepository} / {@link InMemoryDokumentRepository}
* - Methoden: erstelleAuftragsbestaetigung / erstelleLieferschein /
* erstelleRechnung / exportiereAlsPdf
* - Status: OFFEN, UEBERFUEHRT, BESTAETIGT, GELIEFERT, FAKTURIERT,
* FESTGESCHRIEBEN
*
* Die ursprüngliche GUI-Verdrahtung (DokumentenService, getDokumentnummer,
* RECHNUNG_ERSTELLT, deleteDokument, ...) findet sich in keinem Pflichtenheft
* wieder und wurde hier auf die gemeinsame Spec gemappt. OP-6 (GUI vs.
* PH v1.1 / Gruppe F PH v1.0).
*/ */
public class DokumentPanel extends JPanel { public class DokumentPanel extends JPanel {
private final MainFrame mainFrame; private final MainFrame mainFrame;
private final DokumentenService dokumentenService; private final DokumentService dokumentService;
private final TrackingDokumentRepository tracking;
// Tabelle // Tabelle
private DefaultTableModel tableModel; private DefaultTableModel tableModel;
private JTable dokumentTable; private JTable dokumentTable;
// Detailfelder (schreibgeschützt) // Detailfelder (schreibgeschützt)
private JTextField txtDokumentnummer; private JTextField txtDokumentNummer;
private JTextField txtKundenId; private JTextField txtKundenId;
private JTextField txtDatum; private JTextField txtDatum;
private JTextField txtStatus; private JTextField txtStatus;
private JCheckBox chkFestgeschrieben; private JCheckBox chkFestgeschrieben;
// Positionstabelle // Positionstabelle
private DefaultTableModel posTableModel; private DefaultTableModel posTableModel;
@ -42,8 +78,6 @@ public class DokumentPanel extends JPanel {
// Aktions-Buttons (PHGUI10) // Aktions-Buttons (PHGUI10)
private JButton btnNeu; private JButton btnNeu;
private JButton btnSpeichern;
private JButton btnLoeschen;
private JButton btnAbbrechen; private JButton btnAbbrechen;
private JButton btnPdfExport; private JButton btnPdfExport;
@ -56,19 +90,55 @@ public class DokumentPanel extends JPanel {
public DokumentPanel(MainFrame mainFrame) { public DokumentPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame; this.mainFrame = mainFrame;
this.dokumentenService = new DokumentenService(new DokumentRepository());
// DokumentServiceImpl braucht 4 Abhängigkeiten (DI laut Gruppe F):
// Repository, ProductLookup, CustomerLookup, PdfExportService.
this.tracking = new TrackingDokumentRepository(new InMemoryDokumentRepository());
PdfExportService pdfExport = new SimplePdfExportService(Path.of("exports"));
CustomerLookup customerLookup = new KundeCustomerLookup(buildKundeService());
ProductLookup productLookup = stubProductLookup();
this.dokumentService = new DokumentServiceImpl(
tracking, productLookup, customerLookup, pdfExport);
setLayout(new BorderLayout(8, 8)); setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
initComponents(); initComponents();
ladeDokumente(); ladeDokumente();
} }
/**
* KundeService nur als Datenquelle für den CustomerLookup-Adapter
* (K3 aus PH v1.1). Eigener Aufbau, da das Panel self-contained ist
* geteiltes Wiring zwischen Panels ist Bestandteil von OP-6.
*/
private KundeService buildKundeService() {
return new KundeService(
new KundeRepositoryJson(new File("data/kunden.json")),
new KundeValidator(),
(BelegReferenzPruefer) (kundeId -> false));
}
/**
* DokumentPanel kann derzeit keine neuen Angebote anlegen
* (Eingabemaske fehlt im UI), deshalb wird der ProductLookup nicht
* aktiv genutzt. Stub ist ausreichend, damit DokumentServiceImpl
* verdrahtet werden kann.
*/
private ProductLookup stubProductLookup() {
return produktId -> Optional.<ProductDto>empty();
}
// -------------------------------------------------------------------------
// UI-Aufbau
// -------------------------------------------------------------------------
private void initComponents() { private void initComponents() {
// --- Linke Seite: Dokumentenliste --- // --- Linke Seite: Dokumentenliste ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4)); JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(300, 0)); leftPanel.setPreferredSize(new Dimension(320, 0));
String[] cols = {"Nr.", "Typ ▲", "Status"}; String[] cols = {"Nr.", "Typ", "Status"};
tableModel = new DefaultTableModel(cols, 0) { tableModel = new DefaultTableModel(cols, 0) {
public boolean isCellEditable(int r, int c) { return false; } public boolean isCellEditable(int r, int c) { return false; }
}; };
@ -76,14 +146,9 @@ public class DokumentPanel extends JPanel {
// Status-Spalte farbig (PHGUI10) // Status-Spalte farbig (PHGUI10)
public Component prepareRenderer(TableCellRenderer r, int row, int col) { public Component prepareRenderer(TableCellRenderer r, int row, int col) {
Component c = super.prepareRenderer(r, row, col); Component c = super.prepareRenderer(r, row, col);
if (col == 2) { if (col == 2 && !isRowSelected(row)) {
String status = (String) getValueAt(row, col); String status = String.valueOf(getValueAt(row, col));
if (status != null) { c.setForeground(farbeFuerStatus(status));
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 { } else {
c.setForeground(Color.BLACK); c.setForeground(Color.BLACK);
} }
@ -91,292 +156,399 @@ public class DokumentPanel extends JPanel {
} }
}; };
dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Sortierung (PHGUI08)
dokumentTable.setAutoCreateRowSorter(true); dokumentTable.setAutoCreateRowSorter(true);
dokumentTable.getTableHeader().setReorderingAllowed(false);
dokumentTable.getSelectionModel().addListSelectionListener(e -> { dokumentTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehltes(); if (!e.getValueIsAdjusting()) zeigeAusgewaehltes();
}); });
leftPanel.add(new JLabel("Dokumente"), BorderLayout.NORTH);
leftPanel.add(new JScrollPane(dokumentTable), BorderLayout.CENTER); leftPanel.add(new JScrollPane(dokumentTable), BorderLayout.CENTER);
// --- Rechte Seite: Detailansicht --- // --- Rechte Seite: Detailbereich ---
JPanel rightPanel = new JPanel(new BorderLayout(4, 8)); JPanel rightPanel = new JPanel(new BorderLayout(0, 8));
rightPanel.add(kopfPanel(), BorderLayout.NORTH);
rightPanel.add(positionsBereich(), BorderLayout.CENTER);
rightPanel.add(buttonPanel(), BorderLayout.SOUTH);
// Kopfbereich JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
JPanel headerPanel = new JPanel(new GridBagLayout()); split.setDividerLocation(320);
headerPanel.setBorder(BorderFactory.createTitledBorder("Dokumentdetail")); add(split, BorderLayout.CENTER);
}
private JPanel kopfPanel() {
JPanel p = new JPanel(new GridBagLayout());
p.setBorder(BorderFactory.createTitledBorder("Dokumentkopf"));
GridBagConstraints g = new GridBagConstraints(); GridBagConstraints g = new GridBagConstraints();
g.insets = new Insets(3, 4, 3, 4); g.insets = new Insets(4, 6, 2, 6);
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.anchor = GridBagConstraints.WEST;
g.gridx = 0; g.gridy = 1; headerPanel.add(new JLabel("Dokumentnummer"), g); g.fill = GridBagConstraints.HORIZONTAL;
g.gridx = 1; headerPanel.add(new JLabel("Kundennummer"), g); g.weightx = 1.0;
txtDokumentnummer = createReadOnlyField();
txtDokumentNummer = createReadOnlyField();
txtKundenId = createReadOnlyField(); txtKundenId = createReadOnlyField();
g.gridx = 0; g.gridy = 2; headerPanel.add(txtDokumentnummer, g); txtDatum = createReadOnlyField();
g.gridx = 1; headerPanel.add(txtKundenId, g); txtStatus = createReadOnlyField();
chkFestgeschrieben = new JCheckBox("Festgeschrieben (PHGUI11)");
chkFestgeschrieben.setEnabled(false);
g.gridx = 0; g.gridy = 3; headerPanel.add(new JLabel("Datum"), g); int row = 0;
g.gridx = 1; headerPanel.add(new JLabel("Status"), g); row = addLabelField(p, g, "Dokumentnummer:", txtDokumentNummer, row);
txtDatum = createReadOnlyField(); row = addLabelField(p, g, "Kunden-ID:", txtKundenId, row);
txtStatus = createReadOnlyField(); row = addLabelField(p, g, "Datum:", txtDatum, row);
g.gridx = 0; g.gridy = 4; headerPanel.add(txtDatum, g); row = addLabelField(p, g, "Status:", txtStatus, row);
g.gridx = 1; headerPanel.add(txtStatus, g);
rightPanel.add(headerPanel, BorderLayout.NORTH); g.gridx = 0; g.gridy = row; g.gridwidth = 2; p.add(chkFestgeschrieben, g);
g.gridwidth = 1;
return p;
}
// Positionstabelle private JPanel positionsBereich() {
String[] posCols = {"Pos", "Bezeichnung", "Menge", "Einzelpreis", "MwSt.", "Summe"}; JPanel p = new JPanel(new BorderLayout(0, 4));
p.setBorder(BorderFactory.createTitledBorder("Positionen / Summen"));
String[] posCols = {"Pos.", "Bezeichnung", "Menge", "Einzelpreis", "MwSt.", "Gesamt"};
posTableModel = new DefaultTableModel(posCols, 0) { posTableModel = new DefaultTableModel(posCols, 0) {
public boolean isCellEditable(int r, int c) { return false; } public boolean isCellEditable(int r, int c) { return false; }
}; };
posTable = new JTable(posTableModel); posTable = new JTable(posTableModel);
posTable.setEnabled(false); posTable.getTableHeader().setReorderingAllowed(false);
p.add(new JScrollPane(posTable), BorderLayout.CENTER);
JPanel posPanel = new JPanel(new BorderLayout()); JPanel summenPanel = new JPanel(new GridLayout(1, 6, 8, 0));
posPanel.setBorder(BorderFactory.createTitledBorder("Positionen:")); summenPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
posPanel.add(new JScrollPane(posTable), BorderLayout.CENTER); summenPanel.add(new JLabel("Netto:"));
lblNetto = new JLabel("0.00 €");
// Summen summenPanel.add(lblNetto);
JPanel summenPanel = new JPanel(new GridLayout(3, 2, 4, 2)); summenPanel.add(new JLabel("USt.:"));
summenPanel.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8)); lblUst = new JLabel("0.00 €");
lblNetto = new JLabel("0,00 €"); lblNetto.setHorizontalAlignment(SwingConstants.RIGHT); summenPanel.add(lblUst);
lblUst = new JLabel("0,00 €"); lblUst.setHorizontalAlignment(SwingConstants.RIGHT); summenPanel.add(new JLabel("Brutto:"));
lblBrutto = new JLabel("0,00 €", SwingConstants.RIGHT); lblBrutto = new JLabel("0.00 €");
lblBrutto.setFont(new Font("SansSerif", Font.BOLD, 13)); summenPanel.add(lblBrutto);
summenPanel.add(new JLabel("Netto:")); summenPanel.add(lblNetto); p.add(summenPanel, BorderLayout.SOUTH);
summenPanel.add(new JLabel("USt. 19%:")); summenPanel.add(lblUst); return p;
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 JPanel buttonPanel() {
JPanel p = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4));
btnNeu = new JButton("Auswahl zurücksetzen");
btnAbbrechen = new JButton("Abbrechen");
btnPdfExport = new JButton("PDF-Export");
btnAngebotZuAuftrag = new JButton("→ Auftragsbestätigung");
btnAuftragZuLieferschein = new JButton("→ Lieferschein");
btnLieferscheinZuRechnung = new JButton("→ Rechnung");
btnNeu.addActionListener(e -> neuesFormular());
btnAbbrechen.addActionListener(e -> neuesFormular());
btnPdfExport.addActionListener(e -> pdfExport());
btnAngebotZuAuftrag.addActionListener(e ->
folgeaktion(DokumentType.ANGEBOT));
btnAuftragZuLieferschein.addActionListener(e ->
folgeaktion(DokumentType.AUFTRAGSBESTAETIGUNG));
btnLieferscheinZuRechnung.addActionListener(e ->
folgeaktion(DokumentType.LIEFERSCHEIN));
p.add(btnAngebotZuAuftrag);
p.add(btnAuftragZuLieferschein);
p.add(btnLieferscheinZuRechnung);
p.add(btnPdfExport);
p.add(btnNeu);
p.add(btnAbbrechen);
// Initial alles deaktiviert (es ist nichts ausgewählt)
setAktionenDeaktiviert();
return p;
}
private int addLabelField(JPanel p, GridBagConstraints g,
String label, JComponent field, 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);
return row + 1;
}
// -------------------------------------------------------------------------
// Daten laden / anzeigen
// -------------------------------------------------------------------------
private void ladeDokumente() { private void ladeDokumente() {
tableModel.setRowCount(0); tableModel.setRowCount(0);
List<Dokument> alle = dokumentenService.getAllDokumente(); for (Dokument d : tracking.findAll()) {
for (Dokument d : alle) {
tableModel.addRow(new Object[]{ tableModel.addRow(new Object[]{
d.getDokumentnummer(), d.getDokumentNummer(),
d.getTypBezeichnung(), typBezeichnung(d.getTyp()),
d.getStatus().getAnzeigename() d.getStatus().name()
}); });
} }
} }
private void zeigeAusgewaehltes() { private void zeigeAusgewaehltes() {
int row = dokumentTable.getSelectedRow(); int row = dokumentTable.getSelectedRow();
if (row < 0) return; if (row < 0) { aktuellesDokument = null; return; }
int modelRow = dokumentTable.convertRowIndexToModel(row); int modelRow = dokumentTable.convertRowIndexToModel(row);
String nr = (String) tableModel.getValueAt(modelRow, 0); String nr = (String) tableModel.getValueAt(modelRow, 0);
dokumentenService.getDokumentByNummer(nr).ifPresent(d -> { tracking.findByNummer(nr).ifPresent(this::zeigeDokument);
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);
});
} }
private void zeigeDokument(Dokument d) {
aktuellesDokument = d;
txtDokumentNummer.setText(d.getDokumentNummer());
txtKundenId.setText(Long.toString(d.getKundenId()));
txtDatum.setText(d.getDatum() == null ? "" : d.getDatum().toString());
txtStatus.setText(d.getStatus().name());
boolean festgeschrieben = d.getStatus() == DokumentStatus.FESTGESCHRIEBEN;
chkFestgeschrieben.setSelected(festgeschrieben);
// Positionen
posTableModel.setRowCount(0);
int pos = 1;
boolean lieferschein = d.getTyp() == DokumentType.LIEFERSCHEIN; // PH-DP-07
for (DokumentPosition p : d.getPositionen()) {
posTableModel.addRow(new Object[]{
pos++,
p.getBezeichnungSnapshot(),
p.getMenge(),
lieferschein ? "—" : (p.getEinzelpreisNettoSnapshot() + " €"),
lieferschein ? "—" : (formatProzent(p.getMwstSatzSnapshot())),
lieferschein ? "—" : (p.getNettoGesamt() + " €")
});
}
// Summen (Lieferschein zeigt keine Preise PH-DP-07)
if (lieferschein) {
lblNetto.setText("—");
lblUst.setText("—");
lblBrutto.setText("—");
} else {
lblNetto.setText(nullSichBd(d.getNettoSumme()) + " €");
lblUst.setText( nullSichBd(d.getUstSumme()) + " €");
lblBrutto.setText(nullSichBd(d.getBruttoSumme())+ " €");
}
aktualisiereAktionen(d);
}
private String nullSichBd(BigDecimal v) { return v == null ? "0.00" : v.toPlainString(); }
/** /**
* Aktiviert nur die zulässigen Folgeaktionen basierend auf dem Dokumentstatus (PHGUI10). * Aktiviert nur die zulässigen Folgeaktionen basierend auf
* Festgeschriebene Rechnungen: alle Bearbeitungsaktionen deaktiviert (PHGUI11). * Dokumenttyp und -status (PHGUI10).
* Festgeschriebene Rechnungen: alle Aktionen außer PDF-Export
* deaktiviert (PHGUI11).
*/ */
private void aktualisiereAktionen(Dokument d) { private void aktualisiereAktionen(Dokument d) {
DokumentType typ = d.getTyp();
DokumentStatus status = d.getStatus(); DokumentStatus status = d.getStatus();
boolean festgeschrieben = d.isFestgeschrieben();
// PHGUI11: Festgeschriebene Rechnung → alles deaktiviert außer PDF-Export if (status == DokumentStatus.FESTGESCHRIEBEN) {
if (festgeschrieben) {
btnNeu.setEnabled(true);
btnSpeichern.setEnabled(false);
btnLoeschen.setEnabled(false);
btnAbbrechen.setEnabled(true);
btnPdfExport.setEnabled(true); // PHGUI12
btnAngebotZuAuftrag.setEnabled(false); btnAngebotZuAuftrag.setEnabled(false);
btnAuftragZuLieferschein.setEnabled(false); btnAuftragZuLieferschein.setEnabled(false);
btnLieferscheinZuRechnung.setEnabled(false); btnLieferscheinZuRechnung.setEnabled(false);
btnPdfExport.setEnabled(true); // PHGUI12
return; return;
} }
// PHGUI10: Folgeaktionen je nach Status btnPdfExport.setEnabled(true);
btnNeu.setEnabled(true); btnAngebotZuAuftrag.setEnabled(
btnSpeichern.setEnabled(true); typ == DokumentType.ANGEBOT && status == DokumentStatus.OFFEN);
btnLoeschen.setEnabled(true); btnAuftragZuLieferschein.setEnabled(
btnAbbrechen.setEnabled(true); typ == DokumentType.AUFTRAGSBESTAETIGUNG && status == DokumentStatus.BESTAETIGT);
btnPdfExport.setEnabled(status == DokumentStatus.RECHNUNG_ERSTELLT); btnLieferscheinZuRechnung.setEnabled(
typ == DokumentType.LIEFERSCHEIN && status == DokumentStatus.GELIEFERT);
btnAngebotZuAuftrag.setEnabled(status == DokumentStatus.ANGEBOT_OFFEN);
btnAuftragZuLieferschein.setEnabled(status == DokumentStatus.AUFTRAGSBESTAETIGUNG_ERSTELLT);
btnLieferscheinZuRechnung.setEnabled(status == DokumentStatus.LIEFERSCHEIN_ERSTELLT);
} }
private void setAktionenDeaktiviert() { private void setAktionenDeaktiviert() {
btnSpeichern.setEnabled(false);
btnLoeschen.setEnabled(false);
btnPdfExport.setEnabled(false); btnPdfExport.setEnabled(false);
btnAngebotZuAuftrag.setEnabled(false); btnAngebotZuAuftrag.setEnabled(false);
btnAuftragZuLieferschein.setEnabled(false); btnAuftragZuLieferschein.setEnabled(false);
btnLieferscheinZuRechnung.setEnabled(false); btnLieferscheinZuRechnung.setEnabled(false);
} }
// -------------------------------------------------------------------------
// Aktionen
// -------------------------------------------------------------------------
private void neuesFormular() { private void neuesFormular() {
aktuellesDokument = null; aktuellesDokument = null;
txtDokumentnummer.setText(""); txtKundenId.setText(""); dokumentTable.clearSelection();
txtDatum.setText(""); txtStatus.setText(""); txtDokumentNummer.setText("");
txtKundenId.setText("");
txtDatum.setText("");
txtStatus.setText("");
chkFestgeschrieben.setSelected(false); chkFestgeschrieben.setSelected(false);
posTableModel.setRowCount(0); posTableModel.setRowCount(0);
lblNetto.setText("0,00 €"); lblUst.setText("0,00 €"); lblBrutto.setText("0,00 €"); lblNetto.setText("0.00 €");
lblUst.setText("0.00 €");
lblBrutto.setText("0.00 €");
setAktionenDeaktiviert(); setAktionenDeaktiviert();
btnNeu.setEnabled(true); btnAbbrechen.setEnabled(true);
dokumentTable.clearSelection();
} }
private void loeschen() { /**
* Erzeugt das spec-konforme Folgedokument zum aktuell ausgewählten
* Dokument. PH-DP-04 / PH-DP-06 / PH-DP-08.
*/
private void folgeaktion(DokumentType erwartet) {
if (aktuellesDokument == null) { if (aktuellesDokument == null) {
mainFrame.showStatus("Kein Dokument ausgewählt."); mainFrame.showStatus("Kein Dokument ausgewählt.");
return; return;
} }
// PHGUI07: Bestätigungsdialog if (aktuellesDokument.getTyp() != erwartet) {
int confirm = JOptionPane.showConfirmDialog( mainFrame.showStatus("Aktion passt nicht zum Dokumenttyp.");
this, "Dokument wirklich löschen?", "Löschen bestätigen", return;
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.");
} }
} String quellNr = aktuellesDokument.getDokumentNummer();
private void folgeaktion(String typ) {
if (aktuellesDokument == null) return;
try { try {
switch (typ) { Dokument folge;
case "ANGEBOT_ZU_AUFTRAG" -> switch (erwartet) {
dokumentenService.createAuftragsbestaetigung(aktuellesDokument.getDokumentnummer()); case ANGEBOT ->
case "AUFTRAG_ZU_LIEFERSCHEIN" -> folge = dokumentService.erstelleAuftragsbestaetigung(quellNr); // PH-DP-04
dokumentenService.createLieferschein(aktuellesDokument.getDokumentnummer()); case AUFTRAGSBESTAETIGUNG ->
case "LIEFERSCHEIN_ZU_RECHNUNG" -> folge = dokumentService.erstelleLieferschein(quellNr, LocalDate.now()); // PH-DP-06
dokumentenService.createRechnung(aktuellesDokument.getDokumentnummer()); case LIEFERSCHEIN ->
folge = dokumentService.erstelleRechnung(quellNr); // PH-DP-08
default -> { mainFrame.showStatus("Kein Folgedokument vorgesehen."); return; }
} }
ladeDokumente(); ladeDokumente();
mainFrame.showStatus("Folgedokument erfolgreich erstellt."); mainFrame.showStatus("Folgedokument erstellt: " + folge.getDokumentNummer());
} catch (Exception ex) { } catch (RuntimeException ex) {
// Fängt IllegalStateException, DuplicateDocumentException,
// ValidationException, DokumentNotFoundException
mainFrame.showStatus("Fehler: " + ex.getMessage()); mainFrame.showStatus("Fehler: " + ex.getMessage());
} }
} }
private void pdfExport() { private void pdfExport() {
// PHGUI12: PDF-Export auslösen
if (aktuellesDokument == null) { if (aktuellesDokument == null) {
mainFrame.showStatus("Kein Dokument ausgewählt."); mainFrame.showStatus("Kein Dokument ausgewählt.");
return; return;
} }
try { try {
dokumentenService.exportPdf(aktuellesDokument.getDokumentnummer(), aktuellesDokument.getTyp()); Path pfad = dokumentService.exportiereAlsPdf(
mainFrame.showStatus("PDF-Export erfolgreich gestartet."); aktuellesDokument.getDokumentNummer()); // PHGUI12 / PH-DP-12
} catch (Exception ex) { mainFrame.showStatus("PDF erzeugt: " + pfad);
} catch (RuntimeException ex) {
mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage()); mainFrame.showStatus("PDF-Export fehlgeschlagen: " + ex.getMessage());
} }
} }
// -------------------------------------------------------------------------
// Helfer
// -------------------------------------------------------------------------
private static String typBezeichnung(DokumentType t) {
if (t == null) return "";
return switch (t) {
case ANGEBOT -> "Angebot";
case AUFTRAGSBESTAETIGUNG -> "Auftragsbestätigung";
case LIEFERSCHEIN -> "Lieferschein";
case RECHNUNG -> "Rechnung";
};
}
private static Color farbeFuerStatus(String status) {
return switch (status) {
case "OFFEN", "BESTAETIGT" -> new Color(41, 128, 185);
case "GELIEFERT" -> new Color(39, 174, 96);
case "FESTGESCHRIEBEN" -> new Color(155, 89, 182);
case "UEBERFUEHRT", "FAKTURIERT" -> new Color(127, 140, 141);
default -> Color.BLACK;
};
}
private static String formatProzent(BigDecimal mwstSatz) {
if (mwstSatz == null) return "";
return mwstSatz.multiply(BigDecimal.valueOf(100)) + " %";
}
private JTextField createReadOnlyField() { private JTextField createReadOnlyField() {
JTextField f = new JTextField(); JTextField f = new JTextField();
f.setEditable(false); f.setEditable(false);
f.setBackground(new Color(230, 230, 230)); f.setBackground(new Color(230, 230, 230));
return f; return f;
} }
}
// =========================================================================
// Adapter / Wrapper GUI-intern, keine Eingriffe in fremde Module
// =========================================================================
/**
* Adapter K3 (PH v1.1 Kap. 6.2): Bedient die von Gruppe F geforderte
* {@link CustomerLookup}-Schnittstelle über das spec-konforme
* {@link KundeService} der Kundenverwaltung.
*/
private static final class KundeCustomerLookup implements CustomerLookup {
private final KundeService kundeService;
KundeCustomerLookup(KundeService kundeService) {
this.kundeService = kundeService;
}
@Override
public Optional<CustomerDto> findCustomer(long kundenId) {
return kundeService.finde(kundenId).map(this::toDto);
}
private CustomerDto toDto(Kunde k) {
String name;
if (k.getFirmenname() != null && !k.getFirmenname().isBlank()) {
name = k.getFirmenname();
} else {
String n = k.getNachname() == null ? "" : k.getNachname();
name = (k.getVorname() == null || k.getVorname().isBlank())
? n : (k.getVorname() + " " + n);
}
String anschrift = String.format("%s, %s %s",
nullAls(k.getStrasse()),
nullAls(k.getPlz()),
nullAls(k.getOrt()));
return new CustomerDto(k.getKundeId(), name, anschrift, nullAls(k.getUstIdNr()));
}
private static String nullAls(String s) { return s == null ? "" : s; }
}
/**
* Decorator über {@link DokumentRepository}: trackt zusätzlich alle
* gespeicherten Dokumente, damit das Panel eine vollständige Liste
* anzeigen kann. {@code findByNummer/findByKunde/...} werden an das
* gekapselte Repository delegiert.
*
* Notwendig, weil das Schnittstellen-Interface kein {@code findAll()}
* vorsieht (Gruppe F PH v1.0 Kap. 6.2) ein Tracking in der GUI ist
* spec-neutral und greift nicht in Gruppe F's Modul ein.
*/
private static final class TrackingDokumentRepository implements DokumentRepository {
private final DokumentRepository delegate;
private final List<Dokument> alle = new ArrayList<>();
TrackingDokumentRepository(DokumentRepository delegate) {
this.delegate = delegate;
}
List<Dokument> findAll() {
List<Dokument> kopie = new ArrayList<>(alle);
kopie.sort(Comparator
.comparing(Dokument::getDatum, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(Dokument::getDokumentNummer));
return Collections.unmodifiableList(kopie);
}
@Override
public void save(Dokument dokument) {
delegate.save(dokument);
if (!alle.contains(dokument)) {
alle.add(dokument);
}
}
@Override public Optional<Dokument> findByNummer(String nummer) { return delegate.findByNummer(nummer); }
@Override public List<Dokument> findByKunde(long kundenId) { return delegate.findByKunde(kundenId); }
@Override public boolean existsFolgedokument(String quellDokumentNr) { return delegate.existsFolgedokument(quellDokumentNr); }
@Override public boolean existsReferencedProdukt(long produktId) { return delegate.existsReferencedProdukt(produktId); }
@Override public boolean existsReferencedKunde(long kundenId) { return delegate.existsReferencedKunde(kundenId); }
@Override public int findMaxLaufendeNummer(String praefix, int j) { return delegate.findMaxLaufendeNummer(praefix, j); }
}
}

View File

@ -3,29 +3,46 @@ package de.hsmannheim.faktura.gui;
import javax.swing.*; import javax.swing.*;
import javax.swing.table.*; import javax.swing.table.*;
import java.awt.*; import java.awt.*;
import java.io.File;
import java.util.List; import java.util.List;
import de.hsmannheim.faktura.kunde.Kunde; import de.hsmannheim.faktura.kunde.model.Kunde;
import de.hsmannheim.faktura.kunde.service.KundenService; import de.hsmannheim.faktura.kunde.repository.KundeRepository;
import de.hsmannheim.faktura.kunde.repository.KundenRepository; import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson;
import de.hsmannheim.faktura.dokumente.repository.DokumentRepository; import de.hsmannheim.faktura.kunde.service.BelegReferenzPruefer;
import de.hsmannheim.faktura.kunde.service.KundeService;
import de.hsmannheim.faktura.kunde.service.KundeValidator;
import de.hsmannheim.faktura.kunde.service.LoeschsperreException;
import de.hsmannheim.faktura.kunde.service.ValidierungsException;
/** /**
* Panel für die Kundenverwaltung (PHGUI02, PHGUI03, PHGUI05PHGUI09). * Panel für die Kundenverwaltung (PHGUI02, PHGUI03, PHGUI05PHGUI09).
* Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02). *
* Verdrahtung gegen die spec-konforme Schnittstelle K1 aus dem
* gemeinsamen Pflichtenheft v1.1 (Kap. 6.1.2 / Kap. 7.2):
* - Klassennamen: {@link KundeService}, {@link KundeRepository}, {@link Kunde}
* - Methoden: anlegen / bearbeiten / suchen / loeschen / finde / alle
* - Typ kundeId: long
*
* Das ursprüngliche GUI-Modell (KundenService / KundenRepository /
* 8-arg-Konstruktor / getKundenId():int) ist eine Erfindung des
* Gruppen-E-Pflichtenhefts und wird hier auf die gemeinsame Spec
* gemappt. Eintrag im Fahrplan: OP-6 (GUI vs. PH v1.1).
*/ */
public class KundenPanel extends JPanel { public class KundenPanel extends JPanel {
private final MainFrame mainFrame; private final MainFrame mainFrame;
private final KundenService kundenService; private final KundeService kundeService;
// Tabelle // Tabelle
private DefaultTableModel tableModel; private DefaultTableModel tableModel;
private JTable kundenTable; private JTable kundenTable;
// Formularfelder // Formularfelder
private JTextField txtKundenId; private JTextField txtKundeId;
private JTextField txtFirmenname; private JTextField txtFirmenname;
private JTextField txtNachname;
private JTextField txtVorname;
private JTextField txtStrasse; private JTextField txtStrasse;
private JTextField txtPlz; private JTextField txtPlz;
private JTextField txtOrt; private JTextField txtOrt;
@ -34,7 +51,7 @@ public class KundenPanel extends JPanel {
private JTextField txtUstIdNr; private JTextField txtUstIdNr;
// Fehlerhinweise (PHGUI03, PHGUI05) // Fehlerhinweise (PHGUI03, PHGUI05)
private JLabel lblFirmenfehler; private JLabel lblNameFehler;
private JLabel lblStrasseFehler; private JLabel lblStrasseFehler;
private JLabel lblPlzFehler; private JLabel lblPlzFehler;
private JLabel lblOrtFehler; private JLabel lblOrtFehler;
@ -51,9 +68,10 @@ public class KundenPanel extends JPanel {
public KundenPanel(MainFrame mainFrame) { public KundenPanel(MainFrame mainFrame) {
this.mainFrame = mainFrame; this.mainFrame = mainFrame;
this.kundenService = new KundenService( this.kundeService = new KundeService(
new KundenRepository(), new KundeRepositoryJson(new File("data/kunden.json")),
createDummyDokumentRepository() new KundeValidator(),
stubBelegPruefer()
); );
setLayout(new BorderLayout(8, 8)); setLayout(new BorderLayout(8, 8));
setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
@ -61,153 +79,183 @@ public class KundenPanel extends JPanel {
ladeKunden(); 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() { private void initComponents() {
// --- Linke Seite: Liste + Suche --- // --- Linke Seite: Liste + Suche (PHGUI09) ---
JPanel leftPanel = new JPanel(new BorderLayout(4, 4)); JPanel leftPanel = new JPanel(new BorderLayout(4, 4));
leftPanel.setPreferredSize(new Dimension(320, 0)); leftPanel.setPreferredSize(new Dimension(320, 0));
String[] columns = {"ID", "Name ▲", "Ort"}; String[] columns = {"ID", "Name", "Ort"};
tableModel = new DefaultTableModel(columns, 0) { tableModel = new DefaultTableModel(columns, 0) {
public boolean isCellEditable(int r, int c) { return false; } public boolean isCellEditable(int r, int c) { return false; }
}; };
kundenTable = new JTable(tableModel); kundenTable = new JTable(tableModel);
kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Sortierung (PHGUI08) kundenTable.getTableHeader().setReorderingAllowed(false);
kundenTable.setAutoCreateRowSorter(true); kundenTable.setAutoCreateRowSorter(true); // PHGUI08
kundenTable.getSelectionModel().addListSelectionListener(e -> { kundenTable.getSelectionModel().addListSelectionListener(e -> {
if (!e.getValueIsAdjusting()) zeigeAusgewaehlten(); if (!e.getValueIsAdjusting()) zeigeAusgewaehlten();
}); });
leftPanel.add(new JLabel("Kundenliste"), BorderLayout.NORTH); JPanel suchPanel = new JPanel(new BorderLayout(4, 0));
leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER); suchPanel.add(new JLabel("Suche: "), BorderLayout.WEST);
// 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 = new JTextField();
txtSuche.setToolTipText("Name oder ID eingeben...");
txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() {
public void insertUpdate(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 removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); }
}); });
suchePanel.add(txtSuche, BorderLayout.CENTER); suchPanel.add(txtSuche, BorderLayout.CENTER);
leftPanel.add(suchePanel, BorderLayout.SOUTH);
// --- Rechte Seite: Formular --- leftPanel.add(suchPanel, BorderLayout.NORTH);
JPanel rightPanel = new JPanel(new BorderLayout(4, 8)); leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER);
rightPanel.setBorder(BorderFactory.createTitledBorder("Kundendetails Pflichtfelder sind mit * markiert"));
JPanel formPanel = new JPanel(new GridBagLayout()); // --- Rechte Seite: Formular + Buttons (PHGUI02) ---
GridBagConstraints gbc = new GridBagConstraints(); JPanel rightPanel = new JPanel(new BorderLayout(0, 8));
gbc.insets = new Insets(3, 4, 1, 4); rightPanel.add(formularPanel(), BorderLayout.CENTER);
gbc.fill = GridBagConstraints.HORIZONTAL; rightPanel.add(buttonPanel(), BorderLayout.SOUTH);
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); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel);
split.setDividerLocation(320); split.setDividerLocation(320);
add(split, BorderLayout.CENTER); add(split, BorderLayout.CENTER);
} }
// --- Hilfsmethoden Layout --- private JPanel formularPanel() {
private void addLabel(JPanel p, GridBagConstraints g, String text, int col, int row) { JPanel p = new JPanel(new GridBagLayout());
g.gridx = col; g.gridy = row; g.weighty = 0; p.setBorder(BorderFactory.createTitledBorder("Kundendaten"));
p.add(new JLabel(text), g); 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; private int addLabelField(JPanel p, GridBagConstraints g,
p.add(c, 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 --- // --- Daten ---
private void ladeKunden() { private void ladeKunden() {
ladeKundenListe(kundenService.getAllKunden()); ladeKundenListe(kundeService.alle());
} }
private void ladeKundenListe(List<Kunde> liste) { private void ladeKundenListe(List<Kunde> liste) {
tableModel.setRowCount(0); tableModel.setRowCount(0);
for (Kunde k : liste) { for (Kunde k : liste) {
tableModel.addRow(new Object[]{ tableModel.addRow(new Object[]{
String.valueOf(k.getKundenId()), Long.toString(k.getKundeId()),
k.getFirmennameNachname(), nameVon(k),
k.getOrt() 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() { 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(); String query = txtSuche.getText();
ladeKundenListe(kundenService.searchKunden(query)); if (query == null || query.isBlank()) {
ladeKundenListe(kundeService.alle());
} else {
ladeKundenListe(kundeService.suchen(query));
}
} }
private void zeigeAusgewaehlten() { private void zeigeAusgewaehlten() {
@ -215,118 +263,144 @@ public class KundenPanel extends JPanel {
if (row < 0) return; if (row < 0) return;
int modelRow = kundenTable.convertRowIndexToModel(row); int modelRow = kundenTable.convertRowIndexToModel(row);
String idStr = (String) tableModel.getValueAt(modelRow, 0); String idStr = (String) tableModel.getValueAt(modelRow, 0);
kundenService.getKundeById(Integer.parseInt(idStr)).ifPresent(k -> { try {
txtKundenId.setText(String.valueOf(k.getKundenId())); long kundeId = Long.parseLong(idStr);
txtFirmenname.setText(k.getFirmennameNachname()); kundeService.finde(kundeId).ifPresent(this::fuelleFormular);
txtStrasse.setText(k.getStrasse()); } catch (NumberFormatException ignored) {
txtPlz.setText(k.getPlz()); // Tabelle in inkonsistentem Zustand; einfach ignorieren
txtOrt.setText(k.getOrt()); }
txtTelefon.setText(k.getTelefonnummer()); }
txtEmail.setText(k.getEmail());
txtUstIdNr.setText(k.getUstIdNr()); private void fuelleFormular(Kunde k) {
clearFehler(); 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 --- // --- Aktionen ---
private void neuesFormular() { private void neuesFormular() {
txtKundenId.setText(""); txtFirmenname.setText(""); txtStrasse.setText(""); txtKundeId.setText("");
txtPlz.setText(""); txtOrt.setText(""); txtTelefon.setText(""); txtFirmenname.setText(""); txtNachname.setText(""); txtVorname.setText("");
txtEmail.setText(""); txtUstIdNr.setText(""); txtStrasse.setText(""); txtPlz.setText(""); txtOrt.setText("");
txtTelefon.setText(""); txtEmail.setText(""); txtUstIdNr.setText("");
clearFehler(); clearFehler();
kundenTable.clearSelection(); kundenTable.clearSelection();
} }
private void speichern() { private void speichern() {
clearFehler(); clearFehler();
boolean valid = true;
// PHGUI03: Pflichtfelder // GUI-seitige Vorprüfung der Pflichtfelder (PHGUI03) - die fachliche
if (txtFirmenname.getText().trim().isEmpty()) { // Validierung gegen PH v1.1 macht der KundeValidator beim Service-Aufruf.
lblFirmenfehler.setText("⚠ Pflichtfeld darf nicht leer sein"); boolean clientValid = true;
valid = false; if (istLeer(txtFirmenname) && istLeer(txtNachname)) {
} lblNameFehler.setText("⚠ Firmenname ODER Nachname erforderlich");
if (txtStrasse.getText().trim().isEmpty()) { clientValid = false;
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;
} }
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) // Kunde-Objekt aufbauen (no-arg-Konstruktor + Setter; Jackson-kompatibel)
String email = txtEmail.getText().trim(); Kunde k = new Kunde();
if (!email.isEmpty() && !email.matches("^[^@]+@[^@]+\\.[^@]+$")) { if (!txtKundeId.getText().isBlank()) {
lblEmailFehler.setText("⚠ Format: name@domain.tld"); try { k.setKundeId(Long.parseLong(txtKundeId.getText().trim())); }
lblEmailFehler.setForeground(new Color(192, 57, 43)); catch (NumberFormatException ignored) { /* bleibt 0 = Neuanlage */ }
valid = false;
} }
k.setFirmenname(leerAlsNull(txtFirmenname));
if (!valid) return; k.setNachname(leerAlsNull(txtNachname));
k.setVorname(leerAlsNull(txtVorname));
int kundenId = txtKundenId.getText().trim().isEmpty() ? 0 k.setStrasse(leerAlsNull(txtStrasse));
: Integer.parseInt(txtKundenId.getText().trim()); k.setPlz(leerAlsNull(txtPlz));
k.setOrt(leerAlsNull(txtOrt));
Kunde k = new Kunde( k.setTelefon(leerAlsNull(txtTelefon));
kundenId, k.setEmail(leerAlsNull(txtEmail));
txtFirmenname.getText().trim(), k.setUstIdNr(leerAlsNull(txtUstIdNr));
txtStrasse.getText().trim(),
txtPlz.getText().trim(),
txtOrt.getText().trim(),
txtTelefon.getText().trim(),
email,
txtUstIdNr.getText().trim()
);
try { 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(); ladeKunden();
mainFrame.showStatus("Kunde gespeichert (ID " + k.getKundeId() + ")."); // PHGUI06
neuesFormular(); neuesFormular();
// PHGUI06: Bestätigung < 1 Sekunde } catch (ValidierungsException ex) {
mainFrame.showStatus("Kunde erfolgreich gespeichert."); // Fehler aus KundeValidator (PH v1.1 Kap. 6.1.2) sichtbar machen
} catch (IllegalArgumentException ex) { String msg = ex.getMessage();
mainFrame.showStatus("Fehler: " + 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() { private void loeschen() {
int row = kundenTable.getSelectedRow(); int row = kundenTable.getSelectedRow();
if (row < 0) { if (row < 0) { mainFrame.showStatus("Kein Kunde ausgewählt."); return; }
mainFrame.showStatus("Kein Kunde ausgewählt.");
return;
}
// PHGUI07: Bestätigungsdialog // PHGUI07: Bestätigungsdialog
int confirm = JOptionPane.showConfirmDialog( int confirm = JOptionPane.showConfirmDialog(
this, "Kunden wirklich löschen?", "Löschen bestätigen", this, "Kunden wirklich löschen?", "Löschen bestätigen",
JOptionPane.YES_NO_OPTION JOptionPane.YES_NO_OPTION);
);
if (confirm != JOptionPane.YES_OPTION) return; if (confirm != JOptionPane.YES_OPTION) return;
int modelRow = kundenTable.convertRowIndexToModel(row); int modelRow = kundenTable.convertRowIndexToModel(row);
String idStr = (String) tableModel.getValueAt(modelRow, 0); String idStr = (String) tableModel.getValueAt(modelRow, 0);
boolean deleted = kundenService.deleteKunde(Integer.parseInt(idStr)); try {
long kundeId = Long.parseLong(idStr);
if (deleted) { kundeService.loeschen(kundeId); // F-KV-04
ladeKunden(); ladeKunden();
neuesFormular(); neuesFormular();
mainFrame.showStatus("Kunde gelöscht."); mainFrame.showStatus("Kunde gelöscht.");
} else { } catch (LoeschsperreException ex) {
mainFrame.showStatus("Löschen nicht möglich Kunde wird in Dokumenten verwendet."); 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() { private void clearFehler() {
lblFirmenfehler.setText(""); lblNameFehler.setText("");
lblStrasseFehler.setText(""); lblStrasseFehler.setText("");
lblPlzFehler.setText(""); lblPlzFehler.setText("");
lblOrtFehler.setText(""); lblOrtFehler.setText("");
lblEmailFehler.setText("▲ Format: name@domain.tld"); lblEmailFehler.setText(" Format: name@domain.tld");
lblEmailFehler.setForeground(new Color(120, 120, 120)); lblEmailFehler.setForeground(new Color(120, 120, 120));
} }
@ -337,15 +411,10 @@ public class KundenPanel extends JPanel {
return lbl; return lbl;
} }
private DokumentRepository createDummyDokumentRepository() { private JTextField createReadOnlyField() {
return new DokumentRepository() { JTextField f = new JTextField();
public boolean existsReferencedProdukt(long id) { return false; } f.setEditable(false);
public boolean existsReferencedKunde(long id) { return false; } f.setBackground(new Color(230, 230, 230));
public int findMaxLaufendeNummer(String p, int j) { return 0; } return f;
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; }
};
} }
} }