diff --git a/data/kunden.json b/data/kunden.json new file mode 100644 index 0000000..c4bc761 --- /dev/null +++ b/data/kunden.json @@ -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 +} \ No newline at end of file diff --git a/data/products.json b/data/products.json index 1d70858..8666d1b 100644 --- a/data/products.json +++ b/data/products.json @@ -5,4 +5,11 @@ "mwstSatz" : "NORMAL", "beschreibung" : "Schlagbohrer", "kategorie" : "Elektrowerkzeuge" +}, { + "productId" : "PROD-dca63cf9", + "bezeichnung" : "Schraubenschlüssel", + "einzelpreisNetto" : 42.33, + "mwstSatz" : "NORMAL", + "beschreibung" : "Test", + "kategorie" : "Werkzeug" } ] \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceImpl.java b/src/main/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceImpl.java index 0e538c5..abef761 100644 --- a/src/main/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceImpl.java +++ b/src/main/java/de/hsmannheim/faktura/dokumente/service/DokumentServiceImpl.java @@ -38,9 +38,9 @@ public class DokumentServiceImpl implements DokumentService { * Alle Abhängigkeiten werden injiziert (Dependency Injection). */ public DokumentServiceImpl(DokumentRepository repository, - ProductLookup productLookup, - CustomerLookup customerLookup, - PdfExportService pdfExportService) { + ProductLookup productLookup, + CustomerLookup customerLookup, + PdfExportService pdfExportService) { this.repository = repository; this.productLookup = productLookup; this.customerLookup = customerLookup; @@ -113,18 +113,23 @@ public class DokumentServiceImpl implements DokumentService { public Auftragsbestaetigung erstelleAuftragsbestaetigung(String angebotsNummer) { Angebot angebot = ladeAngebot(angebotsNummer); - // Status-Prüfung (PH-DP-04) - if (angebot.getStatus() != DokumentStatus.OFFEN) { - throw new IllegalStateException( - "Angebot " + angebotsNummer + " ist nicht mehr OFFEN (Status: " + - angebot.getStatus() + "). Auftragsbestätigung kann nicht erzeugt werden."); - } - - // Doppelte AB verhindern (PH-DP-05) + // Doppelte AB zuerst (PH-DP-05 / MT-DP-06): wenn bereits eine AB für + // dieses Angebot existiert, ist das Angebot zwangsläufig UEBERFUEHRT + // und die Status-Prüfung würde ebenfalls anschlagen. Die Spec verlangt + // in diesem Szenario eindeutig DuplicateDocumentException, deshalb + // muss diese Prüfung vor dem allgemeinen Status-Check stehen. if (repository.existsFolgedokument(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 Auftragsbestaetigung ab = new Auftragsbestaetigung(); ab.setDokumentNummer(abGenerator.naechsteNummer()); @@ -156,8 +161,8 @@ public class DokumentServiceImpl implements DokumentService { if (ab.getStatus() != DokumentStatus.BESTAETIGT) { throw new IllegalStateException( - "Auftragsbestätigung " + auftragsNummer + " hat nicht den Status BESTAETIGT " + - "(aktueller Status: " + ab.getStatus() + ")."); + "Auftragsbestätigung " + auftragsNummer + " hat nicht den Status BESTAETIGT " + + "(aktueller Status: " + ab.getStatus() + ")."); } // Positionen ohne Preis-Snapshot (PH-DP-07) @@ -189,8 +194,8 @@ public class DokumentServiceImpl implements DokumentService { if (ls.getStatus() != DokumentStatus.GELIEFERT) { throw new IllegalStateException( - "Lieferschein " + lieferscheinNummer + " hat nicht den Status GELIEFERT " + - "(aktueller Status: " + ls.getStatus() + ")."); + "Lieferschein " + lieferscheinNummer + " hat nicht den Status GELIEFERT " + + "(aktueller Status: " + ls.getStatus() + ")."); } // Kundendaten für Pflichtangaben holen @@ -320,4 +325,4 @@ public class DokumentServiceImpl implements DokumentService { pa.setLeistungsdatum(lieferdatum); return pa; } -} +} \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/gui/DokumentPanel.java b/src/main/java/de/hsmannheim/faktura/gui/DokumentPanel.java index 3279f71..ce1aba3 100644 --- a/src/main/java/de/hsmannheim/faktura/gui/DokumentPanel.java +++ b/src/main/java/de/hsmannheim/faktura/gui/DokumentPanel.java @@ -3,33 +3,69 @@ package de.hsmannheim.faktura.gui; import javax.swing.*; import javax.swing.table.*; 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.Optional; 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.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.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). - * 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 { private final MainFrame mainFrame; - private final DokumentenService dokumentenService; + private final DokumentService dokumentService; + private final TrackingDokumentRepository tracking; // Tabelle private DefaultTableModel tableModel; private JTable dokumentTable; // Detailfelder (schreibgeschützt) - private JTextField txtDokumentnummer; + private JTextField txtDokumentNummer; private JTextField txtKundenId; private JTextField txtDatum; private JTextField txtStatus; - private JCheckBox chkFestgeschrieben; + private JCheckBox chkFestgeschrieben; // Positionstabelle private DefaultTableModel posTableModel; @@ -42,8 +78,6 @@ public class DokumentPanel extends JPanel { // Aktions-Buttons (PHGUI10) private JButton btnNeu; - private JButton btnSpeichern; - private JButton btnLoeschen; private JButton btnAbbrechen; private JButton btnPdfExport; @@ -56,19 +90,55 @@ public class DokumentPanel extends JPanel { public DokumentPanel(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)); setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); initComponents(); 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.empty(); + } + + // ------------------------------------------------------------------------- + // UI-Aufbau + // ------------------------------------------------------------------------- + private void initComponents() { // --- Linke Seite: Dokumentenliste --- 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) { public boolean isCellEditable(int r, int c) { return false; } }; @@ -76,14 +146,9 @@ public class DokumentPanel extends JPanel { // 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); - } + if (col == 2 && !isRowSelected(row)) { + String status = String.valueOf(getValueAt(row, col)); + c.setForeground(farbeFuerStatus(status)); } else { c.setForeground(Color.BLACK); } @@ -91,292 +156,399 @@ public class DokumentPanel extends JPanel { } }; dokumentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - // Sortierung (PHGUI08) dokumentTable.setAutoCreateRowSorter(true); + dokumentTable.getTableHeader().setReorderingAllowed(false); 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)); + // --- Rechte Seite: Detailbereich --- + JPanel rightPanel = new JPanel(new BorderLayout(0, 8)); + rightPanel.add(kopfPanel(), BorderLayout.NORTH); + rightPanel.add(positionsBereich(), BorderLayout.CENTER); + rightPanel.add(buttonPanel(), BorderLayout.SOUTH); - // Kopfbereich - JPanel headerPanel = new JPanel(new GridBagLayout()); - headerPanel.setBorder(BorderFactory.createTitledBorder("Dokumentdetail")); + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel); + split.setDividerLocation(320); + add(split, BorderLayout.CENTER); + } + + private JPanel kopfPanel() { + JPanel p = new JPanel(new GridBagLayout()); + p.setBorder(BorderFactory.createTitledBorder("Dokumentkopf")); 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.insets = new Insets(4, 6, 2, 6); 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(); + g.fill = GridBagConstraints.HORIZONTAL; + g.weightx = 1.0; + + txtDokumentNummer = createReadOnlyField(); txtKundenId = createReadOnlyField(); - g.gridx = 0; g.gridy = 2; headerPanel.add(txtDokumentnummer, g); - g.gridx = 1; headerPanel.add(txtKundenId, g); + txtDatum = createReadOnlyField(); + txtStatus = createReadOnlyField(); + chkFestgeschrieben = new JCheckBox("Festgeschrieben (PHGUI11)"); + chkFestgeschrieben.setEnabled(false); - 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); + int row = 0; + row = addLabelField(p, g, "Dokumentnummer:", txtDokumentNummer, row); + row = addLabelField(p, g, "Kunden-ID:", txtKundenId, row); + row = addLabelField(p, g, "Datum:", txtDatum, row); + row = addLabelField(p, g, "Status:", txtStatus, row); - rightPanel.add(headerPanel, BorderLayout.NORTH); + g.gridx = 0; g.gridy = row; g.gridwidth = 2; p.add(chkFestgeschrieben, g); + g.gridwidth = 1; + return p; + } - // Positionstabelle - String[] posCols = {"Pos", "Bezeichnung", "Menge", "Einzelpreis", "MwSt.", "Summe"}; + private JPanel positionsBereich() { + 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) { public boolean isCellEditable(int r, int c) { return false; } }; posTable = new JTable(posTableModel); - posTable.setEnabled(false); + posTable.getTableHeader().setReorderingAllowed(false); + p.add(new JScrollPane(posTable), BorderLayout.CENTER); - 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); + JPanel summenPanel = new JPanel(new GridLayout(1, 6, 8, 0)); + summenPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + summenPanel.add(new JLabel("Netto:")); + lblNetto = new JLabel("0.00 €"); + summenPanel.add(lblNetto); + summenPanel.add(new JLabel("USt.:")); + lblUst = new JLabel("0.00 €"); + summenPanel.add(lblUst); + summenPanel.add(new JLabel("Brutto:")); + lblBrutto = new JLabel("0.00 €"); + summenPanel.add(lblBrutto); + p.add(summenPanel, BorderLayout.SOUTH); + return p; } - // --- 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() { tableModel.setRowCount(0); - List alle = dokumentenService.getAllDokumente(); - for (Dokument d : alle) { + for (Dokument d : tracking.findAll()) { tableModel.addRow(new Object[]{ - d.getDokumentnummer(), - d.getTypBezeichnung(), - d.getStatus().getAnzeigename() + d.getDokumentNummer(), + typBezeichnung(d.getTyp()), + d.getStatus().name() }); } } private void zeigeAusgewaehltes() { int row = dokumentTable.getSelectedRow(); - if (row < 0) return; + if (row < 0) { aktuellesDokument = null; 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); - }); + tracking.findByNummer(nr).ifPresent(this::zeigeDokument); } + 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). - * Festgeschriebene Rechnungen: alle Bearbeitungsaktionen deaktiviert (PHGUI11). + * Aktiviert nur die zulässigen Folgeaktionen basierend auf + * Dokumenttyp und -status (PHGUI10). + * Festgeschriebene Rechnungen: alle Aktionen außer PDF-Export + * deaktiviert (PHGUI11). */ private void aktualisiereAktionen(Dokument d) { + DokumentType typ = d.getTyp(); 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 + if (status == DokumentStatus.FESTGESCHRIEBEN) { btnAngebotZuAuftrag.setEnabled(false); btnAuftragZuLieferschein.setEnabled(false); btnLieferscheinZuRechnung.setEnabled(false); + btnPdfExport.setEnabled(true); // PHGUI12 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); + btnPdfExport.setEnabled(true); + btnAngebotZuAuftrag.setEnabled( + typ == DokumentType.ANGEBOT && status == DokumentStatus.OFFEN); + btnAuftragZuLieferschein.setEnabled( + typ == DokumentType.AUFTRAGSBESTAETIGUNG && status == DokumentStatus.BESTAETIGT); + btnLieferscheinZuRechnung.setEnabled( + typ == DokumentType.LIEFERSCHEIN && status == DokumentStatus.GELIEFERT); } private void setAktionenDeaktiviert() { - btnSpeichern.setEnabled(false); - btnLoeschen.setEnabled(false); btnPdfExport.setEnabled(false); btnAngebotZuAuftrag.setEnabled(false); btnAuftragZuLieferschein.setEnabled(false); btnLieferscheinZuRechnung.setEnabled(false); } + // ------------------------------------------------------------------------- + // Aktionen + // ------------------------------------------------------------------------- + private void neuesFormular() { aktuellesDokument = null; - txtDokumentnummer.setText(""); txtKundenId.setText(""); - txtDatum.setText(""); txtStatus.setText(""); + dokumentTable.clearSelection(); + 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 €"); + lblNetto.setText("0.00 €"); + lblUst.setText("0.00 €"); + lblBrutto.setText("0.00 €"); 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) { 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."); + if (aktuellesDokument.getTyp() != erwartet) { + mainFrame.showStatus("Aktion passt nicht zum Dokumenttyp."); + return; } - } - - private void folgeaktion(String typ) { - if (aktuellesDokument == null) return; + String quellNr = aktuellesDokument.getDokumentNummer(); 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()); + Dokument folge; + switch (erwartet) { + case ANGEBOT -> + folge = dokumentService.erstelleAuftragsbestaetigung(quellNr); // PH-DP-04 + case AUFTRAGSBESTAETIGUNG -> + folge = dokumentService.erstelleLieferschein(quellNr, LocalDate.now()); // PH-DP-06 + case LIEFERSCHEIN -> + folge = dokumentService.erstelleRechnung(quellNr); // PH-DP-08 + default -> { mainFrame.showStatus("Kein Folgedokument vorgesehen."); return; } } ladeDokumente(); - mainFrame.showStatus("Folgedokument erfolgreich erstellt."); - } catch (Exception ex) { + mainFrame.showStatus("Folgedokument erstellt: " + folge.getDokumentNummer()); + } catch (RuntimeException ex) { + // Fängt IllegalStateException, DuplicateDocumentException, + // ValidationException, DokumentNotFoundException 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) { + Path pfad = dokumentService.exportiereAlsPdf( + aktuellesDokument.getDokumentNummer()); // PHGUI12 / PH-DP-12 + mainFrame.showStatus("PDF erzeugt: " + pfad); + } catch (RuntimeException ex) { 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() { JTextField f = new JTextField(); f.setEditable(false); f.setBackground(new Color(230, 230, 230)); 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 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 alle = new ArrayList<>(); + + TrackingDokumentRepository(DokumentRepository delegate) { + this.delegate = delegate; + } + + List findAll() { + List 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 findByNummer(String nummer) { return delegate.findByNummer(nummer); } + @Override public List 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); } + } +} \ No newline at end of file diff --git a/src/main/java/de/hsmannheim/faktura/gui/KundenPanel.java b/src/main/java/de/hsmannheim/faktura/gui/KundenPanel.java index 89b927c..c984c32 100644 --- a/src/main/java/de/hsmannheim/faktura/gui/KundenPanel.java +++ b/src/main/java/de/hsmannheim/faktura/gui/KundenPanel.java @@ -3,29 +3,46 @@ package de.hsmannheim.faktura.gui; import javax.swing.*; import javax.swing.table.*; import java.awt.*; +import java.io.File; import java.util.List; -import de.hsmannheim.faktura.kunde.Kunde; -import de.hsmannheim.faktura.kunde.service.KundenService; -import de.hsmannheim.faktura.kunde.repository.KundenRepository; -import de.hsmannheim.faktura.dokumente.repository.DokumentRepository; +import de.hsmannheim.faktura.kunde.model.Kunde; +import de.hsmannheim.faktura.kunde.repository.KundeRepository; +import de.hsmannheim.faktura.kunde.repository.KundeRepositoryJson; +import de.hsmannheim.faktura.kunde.service.BelegReferenzPruefer; +import de.hsmannheim.faktura.kunde.service.KundeService; +import de.hsmannheim.faktura.kunde.service.KundeValidator; +import de.hsmannheim.faktura.kunde.service.LoeschsperreException; +import de.hsmannheim.faktura.kunde.service.ValidierungsException; /** * Panel für die Kundenverwaltung (PHGUI02, PHGUI03, PHGUI05–PHGUI09). - * Einheitliches Layout mit Navigationsbereich, Inhaltsbereich und Statusbereich (PHGUI02). + * + * Verdrahtung gegen die spec-konforme Schnittstelle K1 aus dem + * gemeinsamen Pflichtenheft v1.1 (Kap. 6.1.2 / Kap. 7.2): + * - Klassennamen: {@link KundeService}, {@link KundeRepository}, {@link Kunde} + * - Methoden: anlegen / bearbeiten / suchen / loeschen / finde / alle + * - Typ kundeId: long + * + * Das ursprüngliche GUI-Modell (KundenService / KundenRepository / + * 8-arg-Konstruktor / getKundenId():int) ist eine Erfindung des + * Gruppen-E-Pflichtenhefts und wird hier auf die gemeinsame Spec + * gemappt. Eintrag im Fahrplan: OP-6 (GUI vs. PH v1.1). */ public class KundenPanel extends JPanel { private final MainFrame mainFrame; - private final KundenService kundenService; + private final KundeService kundeService; // Tabelle private DefaultTableModel tableModel; private JTable kundenTable; // Formularfelder - private JTextField txtKundenId; + private JTextField txtKundeId; private JTextField txtFirmenname; + private JTextField txtNachname; + private JTextField txtVorname; private JTextField txtStrasse; private JTextField txtPlz; private JTextField txtOrt; @@ -34,7 +51,7 @@ public class KundenPanel extends JPanel { private JTextField txtUstIdNr; // Fehlerhinweise (PHGUI03, PHGUI05) - private JLabel lblFirmenfehler; + private JLabel lblNameFehler; private JLabel lblStrasseFehler; private JLabel lblPlzFehler; private JLabel lblOrtFehler; @@ -51,9 +68,10 @@ public class KundenPanel extends JPanel { public KundenPanel(MainFrame mainFrame) { this.mainFrame = mainFrame; - this.kundenService = new KundenService( - new KundenRepository(), - createDummyDokumentRepository() + this.kundeService = new KundeService( + new KundeRepositoryJson(new File("data/kunden.json")), + new KundeValidator(), + stubBelegPruefer() ); setLayout(new BorderLayout(8, 8)); setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); @@ -61,153 +79,183 @@ public class KundenPanel extends JPanel { ladeKunden(); } + /** + * GR-05-Löschsperre lokal deaktiviert: das DokumentPanel hat eine + * eigene Repository-Instanz, eine geteilte Belegsicht erfordert ein + * zentrales Wiring (über MainFrame) und ist Bestandteil von OP-6. + */ + private BelegReferenzPruefer stubBelegPruefer() { + return kundeId -> false; + } + private void initComponents() { - // --- Linke Seite: Liste + Suche --- + // --- Linke Seite: Liste + Suche (PHGUI09) --- JPanel leftPanel = new JPanel(new BorderLayout(4, 4)); leftPanel.setPreferredSize(new Dimension(320, 0)); - String[] columns = {"ID", "Name ▲", "Ort"}; + String[] columns = {"ID", "Name", "Ort"}; tableModel = new DefaultTableModel(columns, 0) { public boolean isCellEditable(int r, int c) { return false; } }; kundenTable = new JTable(tableModel); kundenTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - // Sortierung (PHGUI08) - kundenTable.setAutoCreateRowSorter(true); + kundenTable.getTableHeader().setReorderingAllowed(false); + kundenTable.setAutoCreateRowSorter(true); // PHGUI08 kundenTable.getSelectionModel().addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) zeigeAusgewaehlten(); }); - leftPanel.add(new JLabel("Kundenliste"), BorderLayout.NORTH); - leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER); - - // Suche (PHGUI09: nach Name oder Kunden-ID) - JPanel suchePanel = new JPanel(new BorderLayout(4, 0)); - suchePanel.setBorder(BorderFactory.createTitledBorder("↑ Suche (Name / Kunden-ID)")); + JPanel suchPanel = new JPanel(new BorderLayout(4, 0)); + suchPanel.add(new JLabel("Suche: "), BorderLayout.WEST); txtSuche = new JTextField(); - txtSuche.setToolTipText("Name oder ID eingeben..."); txtSuche.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } - public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } + public void insertUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } + public void removeUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } public void changedUpdate(javax.swing.event.DocumentEvent e) { sucheAktualisieren(); } }); - suchePanel.add(txtSuche, BorderLayout.CENTER); - leftPanel.add(suchePanel, BorderLayout.SOUTH); + suchPanel.add(txtSuche, BorderLayout.CENTER); - // --- Rechte Seite: Formular --- - JPanel rightPanel = new JPanel(new BorderLayout(4, 8)); - rightPanel.setBorder(BorderFactory.createTitledBorder("Kundendetails Pflichtfelder sind mit * markiert")); + leftPanel.add(suchPanel, BorderLayout.NORTH); + leftPanel.add(new JScrollPane(kundenTable), BorderLayout.CENTER); - JPanel formPanel = new JPanel(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(3, 4, 1, 4); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 0.5; - - // Zeile 0: KundenId + Telefon - addLabel(formPanel, gbc, "Kunden-ID (autom.)", 0, 0); - addLabel(formPanel, gbc, "Telefonnummer", 1, 0); - txtKundenId = new JTextField(); txtKundenId.setEditable(false); - txtKundenId.setBackground(new Color(230, 230, 230)); - addField(formPanel, gbc, txtKundenId, 0, 1); - txtTelefon = new JTextField(); - addField(formPanel, gbc, txtTelefon, 1, 1); - - // Zeile 2: Firmenname + E-Mail - addLabel(formPanel, gbc, "Firmenname / Nachname *", 0, 2); - addLabel(formPanel, gbc, "E-Mail", 1, 2); - txtFirmenname = new JTextField(); - addField(formPanel, gbc, txtFirmenname, 0, 3); - txtEmail = new JTextField(); - addField(formPanel, gbc, txtEmail, 1, 3); - - // Fehler Firmenname + E-Mail - lblFirmenfehler = createFehlerLabel(); - addField(formPanel, gbc, lblFirmenfehler, 0, 4); - lblEmailFehler = createFehlerLabel(); - lblEmailFehler.setText("▲ Format: name@domain.tld"); - lblEmailFehler.setForeground(new Color(120, 120, 120)); - addField(formPanel, gbc, lblEmailFehler, 1, 4); - - // Zeile 5: Straße + UStIdNr - addLabel(formPanel, gbc, "Straße *", 0, 5); - addLabel(formPanel, gbc, "UStIdNr.", 1, 5); - txtStrasse = new JTextField(); - addField(formPanel, gbc, txtStrasse, 0, 6); - txtUstIdNr = new JTextField(); - addField(formPanel, gbc, txtUstIdNr, 1, 6); - lblStrasseFehler = createFehlerLabel(); - addField(formPanel, gbc, lblStrasseFehler, 0, 7); - - // Zeile 8: PLZ - addLabel(formPanel, gbc, "PLZ *", 0, 8); - txtPlz = new JTextField(); - addField(formPanel, gbc, txtPlz, 0, 9); - lblPlzFehler = createFehlerLabel(); - addField(formPanel, gbc, lblPlzFehler, 0, 10); - - // Zeile 11: Ort - addLabel(formPanel, gbc, "Ort *", 0, 11); - txtOrt = new JTextField(); - addField(formPanel, gbc, txtOrt, 0, 12); - lblOrtFehler = createFehlerLabel(); - addField(formPanel, gbc, lblOrtFehler, 0, 13); - - rightPanel.add(new JScrollPane(formPanel), BorderLayout.CENTER); - - // --- Buttons (PHGUI02) --- - JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - btnNeu = new JButton("Neu"); - btnSpeichern = new JButton("Speichern"); - btnLoeschen = new JButton("Löschen"); - btnAbbrechen = new JButton("Abbrechen"); - btnPanel.add(btnNeu); - btnPanel.add(btnSpeichern); - btnPanel.add(btnLoeschen); - btnPanel.add(btnAbbrechen); - rightPanel.add(btnPanel, BorderLayout.SOUTH); - - btnNeu.addActionListener(e -> neuesFormular()); - btnSpeichern.addActionListener(e -> speichern()); - btnLoeschen.addActionListener(e -> loeschen()); - btnAbbrechen.addActionListener(e -> { neuesFormular(); ladeKunden(); }); + // --- Rechte Seite: Formular + Buttons (PHGUI02) --- + JPanel rightPanel = new JPanel(new BorderLayout(0, 8)); + rightPanel.add(formularPanel(), BorderLayout.CENTER); + rightPanel.add(buttonPanel(), BorderLayout.SOUTH); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel); split.setDividerLocation(320); add(split, BorderLayout.CENTER); } - // --- Hilfsmethoden Layout --- - private void addLabel(JPanel p, GridBagConstraints g, String text, int col, int row) { - g.gridx = col; g.gridy = row; g.weighty = 0; - p.add(new JLabel(text), g); + private JPanel formularPanel() { + JPanel p = new JPanel(new GridBagLayout()); + p.setBorder(BorderFactory.createTitledBorder("Kundendaten")); + GridBagConstraints g = new GridBagConstraints(); + g.insets = new Insets(4, 6, 2, 6); + g.anchor = GridBagConstraints.WEST; + g.fill = GridBagConstraints.HORIZONTAL; + g.weightx = 1.0; + + txtKundeId = createReadOnlyField(); + txtFirmenname = new JTextField(); + txtNachname = new JTextField(); + txtVorname = new JTextField(); + txtStrasse = new JTextField(); + txtPlz = new JTextField(); + txtOrt = new JTextField(); + txtTelefon = new JTextField(); + txtEmail = new JTextField(); + txtUstIdNr = new JTextField(); + + lblNameFehler = createFehlerLabel(); + lblStrasseFehler = createFehlerLabel(); + lblPlzFehler = createFehlerLabel(); + lblOrtFehler = createFehlerLabel(); + lblEmailFehler = createFehlerLabel(); + lblEmailFehler.setText("ⓘ Format: name@domain.tld"); + lblEmailFehler.setForeground(new Color(120, 120, 120)); + + int row = 0; + row = addLabelField(p, g, "Kunden-ID:", txtKundeId, null, row); + row = addLabelField(p, g, "Firmenname *:", txtFirmenname, lblNameFehler, row); + row = addLabelField(p, g, "Nachname *:", txtNachname, null, row); + row = addLabelField(p, g, "Vorname:", txtVorname, null, row); + row = addLabelField(p, g, "Straße + Nr. *:", txtStrasse, lblStrasseFehler, row); + row = addLabelField(p, g, "PLZ *:", txtPlz, lblPlzFehler, row); + row = addLabelField(p, g, "Ort *:", txtOrt, lblOrtFehler, row); + row = addLabelField(p, g, "Telefon:", txtTelefon, null, row); + row = addLabelField(p, g, "E-Mail:", txtEmail, lblEmailFehler, row); + row = addLabelField(p, g, "USt-IdNr.:", txtUstIdNr, null, row); + + // Fülldummy, damit obere Felder oben bleiben + g.gridx = 0; g.gridy = row; g.weighty = 1.0; + p.add(new JLabel(" "), g); + + JLabel hint = new JLabel("* Pflichtfeld – Firmenname ODER Nachname ist Pflicht (PH v1.1 Kap. 6.1.2)"); + hint.setFont(new Font("SansSerif", Font.ITALIC, 11)); + hint.setForeground(new Color(120, 120, 120)); + + JPanel wrap = new JPanel(new BorderLayout()); + wrap.add(p, BorderLayout.CENTER); + wrap.add(hint, BorderLayout.SOUTH); + return wrap; } - private void addField(JPanel p, GridBagConstraints g, JComponent c, int col, int row) { - g.gridx = col; g.gridy = row; g.weighty = 0; - p.add(c, g); + + private int addLabelField(JPanel p, GridBagConstraints g, + String label, JComponent field, JLabel fehlerLabel, int row) { + g.weighty = 0; + g.gridx = 0; g.gridy = row; g.weightx = 0; p.add(new JLabel(label), g); + g.gridx = 1; g.gridy = row; g.weightx = 1.0; p.add(field, g); + row++; + if (fehlerLabel != null) { + g.gridx = 1; g.gridy = row; g.weightx = 1.0; p.add(fehlerLabel, g); + row++; + } + return row; + } + + private JPanel buttonPanel() { + JPanel p = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 4)); + btnNeu = new JButton("Neu"); + btnSpeichern = new JButton("Speichern"); + btnLoeschen = new JButton("Löschen"); + btnAbbrechen = new JButton("Abbrechen"); + + btnNeu.addActionListener(e -> neuesFormular()); + btnSpeichern.addActionListener(e -> speichern()); + btnLoeschen.addActionListener(e -> loeschen()); + btnAbbrechen.addActionListener(e -> { neuesFormular(); kundenTable.clearSelection(); }); + + p.add(btnNeu); p.add(btnSpeichern); p.add(btnLoeschen); p.add(btnAbbrechen); + return p; } // --- Daten --- + private void ladeKunden() { - ladeKundenListe(kundenService.getAllKunden()); + ladeKundenListe(kundeService.alle()); } private void ladeKundenListe(List liste) { tableModel.setRowCount(0); for (Kunde k : liste) { tableModel.addRow(new Object[]{ - String.valueOf(k.getKundenId()), - k.getFirmennameNachname(), - k.getOrt() + Long.toString(k.getKundeId()), + nameVon(k), + nullAls(k.getOrt()) }); } } + /** + * Anzeigename gemäß PH v1.1 Kap. 6.1.2: firmenname ODER nachname ist + * Pflicht. Liefert firmenname, sonst „nachname[, vorname]“. + */ + private static String nameVon(Kunde k) { + if (k.getFirmenname() != null && !k.getFirmenname().isBlank()) { + return k.getFirmenname(); + } + String n = nullAls(k.getNachname()); + if (k.getVorname() != null && !k.getVorname().isBlank()) { + return n + ", " + k.getVorname(); + } + return n; + } + + private static String nullAls(String s) { return s == null ? "" : s; } + private void sucheAktualisieren() { - // PHGUI09: Groß-/Kleinschreibung ignorieren, nach Name oder ID suchen + // PHGUI09: Treffer über Name UND Kundennummer. + // KundeService.suchen("") liefert bewusst eine leere Liste (P4-3), + // deshalb hier explizit auf .alle() umschalten. String query = txtSuche.getText(); - ladeKundenListe(kundenService.searchKunden(query)); + if (query == null || query.isBlank()) { + ladeKundenListe(kundeService.alle()); + } else { + ladeKundenListe(kundeService.suchen(query)); + } } private void zeigeAusgewaehlten() { @@ -215,118 +263,144 @@ public class KundenPanel extends JPanel { if (row < 0) return; int modelRow = kundenTable.convertRowIndexToModel(row); String idStr = (String) tableModel.getValueAt(modelRow, 0); - kundenService.getKundeById(Integer.parseInt(idStr)).ifPresent(k -> { - txtKundenId.setText(String.valueOf(k.getKundenId())); - txtFirmenname.setText(k.getFirmennameNachname()); - txtStrasse.setText(k.getStrasse()); - txtPlz.setText(k.getPlz()); - txtOrt.setText(k.getOrt()); - txtTelefon.setText(k.getTelefonnummer()); - txtEmail.setText(k.getEmail()); - txtUstIdNr.setText(k.getUstIdNr()); - clearFehler(); - }); + try { + long kundeId = Long.parseLong(idStr); + kundeService.finde(kundeId).ifPresent(this::fuelleFormular); + } catch (NumberFormatException ignored) { + // Tabelle in inkonsistentem Zustand; einfach ignorieren + } + } + + private void fuelleFormular(Kunde k) { + txtKundeId.setText(Long.toString(k.getKundeId())); + txtFirmenname.setText(nullAls(k.getFirmenname())); + txtNachname.setText(nullAls(k.getNachname())); + txtVorname.setText(nullAls(k.getVorname())); + txtStrasse.setText(nullAls(k.getStrasse())); + txtPlz.setText(nullAls(k.getPlz())); + txtOrt.setText(nullAls(k.getOrt())); + txtTelefon.setText(nullAls(k.getTelefon())); + txtEmail.setText(nullAls(k.getEmail())); + txtUstIdNr.setText(nullAls(k.getUstIdNr())); + clearFehler(); } // --- Aktionen --- + private void neuesFormular() { - txtKundenId.setText(""); txtFirmenname.setText(""); txtStrasse.setText(""); - txtPlz.setText(""); txtOrt.setText(""); txtTelefon.setText(""); - txtEmail.setText(""); txtUstIdNr.setText(""); + txtKundeId.setText(""); + txtFirmenname.setText(""); txtNachname.setText(""); txtVorname.setText(""); + txtStrasse.setText(""); txtPlz.setText(""); txtOrt.setText(""); + txtTelefon.setText(""); txtEmail.setText(""); txtUstIdNr.setText(""); clearFehler(); kundenTable.clearSelection(); } private void speichern() { clearFehler(); - boolean valid = true; - // PHGUI03: Pflichtfelder - if (txtFirmenname.getText().trim().isEmpty()) { - lblFirmenfehler.setText("⚠ Pflichtfeld darf nicht leer sein"); - valid = false; - } - if (txtStrasse.getText().trim().isEmpty()) { - lblStrasseFehler.setText("⚠ Pflichtfeld darf nicht leer sein"); - valid = false; - } - if (txtPlz.getText().trim().isEmpty()) { - lblPlzFehler.setText("⚠ Pflichtfeld darf nicht leer sein"); - valid = false; - } - if (txtOrt.getText().trim().isEmpty()) { - lblOrtFehler.setText("⚠ Pflichtfeld darf nicht leer sein"); - valid = false; + // GUI-seitige Vorprüfung der Pflichtfelder (PHGUI03) - die fachliche + // Validierung gegen PH v1.1 macht der KundeValidator beim Service-Aufruf. + boolean clientValid = true; + if (istLeer(txtFirmenname) && istLeer(txtNachname)) { + lblNameFehler.setText("⚠ Firmenname ODER Nachname erforderlich"); + clientValid = false; } + if (istLeer(txtStrasse)) { lblStrasseFehler.setText("⚠ Pflichtfeld"); clientValid = false; } + if (istLeer(txtPlz)) { lblPlzFehler.setText("⚠ Pflichtfeld"); clientValid = false; } + if (istLeer(txtOrt)) { lblOrtFehler.setText("⚠ Pflichtfeld"); clientValid = false; } + if (!clientValid) return; - // PHGUI05: E-Mail-Validierung (Format: name@domain.tld) - String email = txtEmail.getText().trim(); - if (!email.isEmpty() && !email.matches("^[^@]+@[^@]+\\.[^@]+$")) { - lblEmailFehler.setText("⚠ Format: name@domain.tld"); - lblEmailFehler.setForeground(new Color(192, 57, 43)); - valid = false; + // Kunde-Objekt aufbauen (no-arg-Konstruktor + Setter; Jackson-kompatibel) + Kunde k = new Kunde(); + if (!txtKundeId.getText().isBlank()) { + try { k.setKundeId(Long.parseLong(txtKundeId.getText().trim())); } + catch (NumberFormatException ignored) { /* bleibt 0 = Neuanlage */ } } - - if (!valid) return; - - int kundenId = txtKundenId.getText().trim().isEmpty() ? 0 - : Integer.parseInt(txtKundenId.getText().trim()); - - Kunde k = new Kunde( - kundenId, - txtFirmenname.getText().trim(), - txtStrasse.getText().trim(), - txtPlz.getText().trim(), - txtOrt.getText().trim(), - txtTelefon.getText().trim(), - email, - txtUstIdNr.getText().trim() - ); + k.setFirmenname(leerAlsNull(txtFirmenname)); + k.setNachname(leerAlsNull(txtNachname)); + k.setVorname(leerAlsNull(txtVorname)); + k.setStrasse(leerAlsNull(txtStrasse)); + k.setPlz(leerAlsNull(txtPlz)); + k.setOrt(leerAlsNull(txtOrt)); + k.setTelefon(leerAlsNull(txtTelefon)); + k.setEmail(leerAlsNull(txtEmail)); + k.setUstIdNr(leerAlsNull(txtUstIdNr)); try { - kundenService.saveKunde(k); + if (k.getKundeId() == 0) { + long neueId = kundeService.anlegen(k); // F-KV-01 + k.setKundeId(neueId); + } else { + kundeService.bearbeiten(k); // F-KV-02 + } ladeKunden(); + mainFrame.showStatus("Kunde gespeichert (ID " + k.getKundeId() + ")."); // PHGUI06 neuesFormular(); - // PHGUI06: Bestätigung < 1 Sekunde - mainFrame.showStatus("Kunde erfolgreich gespeichert."); - } catch (IllegalArgumentException ex) { - mainFrame.showStatus("Fehler: " + ex.getMessage()); + } catch (ValidierungsException ex) { + // Fehler aus KundeValidator (PH v1.1 Kap. 6.1.2) sichtbar machen + String msg = ex.getMessage(); + if (msg.toLowerCase().contains("email") || msg.toLowerCase().contains("e-mail")) { + lblEmailFehler.setText("⚠ " + msg); + lblEmailFehler.setForeground(new Color(192, 57, 43)); + } else if (msg.toLowerCase().contains("firmen") || msg.toLowerCase().contains("nach")) { + lblNameFehler.setText("⚠ " + msg); + } else if (msg.toLowerCase().contains("plz")) { + lblPlzFehler.setText("⚠ " + msg); + } else if (msg.toLowerCase().contains("straße") || msg.toLowerCase().contains("strasse")) { + lblStrasseFehler.setText("⚠ " + msg); + } else if (msg.toLowerCase().contains("ort")) { + lblOrtFehler.setText("⚠ " + msg); + } else { + mainFrame.showStatus("Validierung: " + msg); + } } } private void loeschen() { int row = kundenTable.getSelectedRow(); - if (row < 0) { - mainFrame.showStatus("Kein Kunde ausgewählt."); - return; - } + if (row < 0) { mainFrame.showStatus("Kein Kunde ausgewählt."); return; } // PHGUI07: Bestätigungsdialog int confirm = JOptionPane.showConfirmDialog( - this, "Kunden wirklich löschen?", "Löschen bestätigen", - JOptionPane.YES_NO_OPTION - ); + this, "Kunden wirklich löschen?", "Löschen bestätigen", + JOptionPane.YES_NO_OPTION); if (confirm != JOptionPane.YES_OPTION) return; int modelRow = kundenTable.convertRowIndexToModel(row); String idStr = (String) tableModel.getValueAt(modelRow, 0); - boolean deleted = kundenService.deleteKunde(Integer.parseInt(idStr)); - - if (deleted) { + try { + long kundeId = Long.parseLong(idStr); + kundeService.loeschen(kundeId); // F-KV-04 ladeKunden(); neuesFormular(); mainFrame.showStatus("Kunde gelöscht."); - } else { - mainFrame.showStatus("Löschen nicht möglich – Kunde wird in Dokumenten verwendet."); + } catch (LoeschsperreException ex) { + mainFrame.showStatus("Löschen nicht möglich – " + ex.getMessage()); // GR-05 + } catch (ValidierungsException ex) { + mainFrame.showStatus("Fehler: " + ex.getMessage()); + } catch (NumberFormatException ex) { + mainFrame.showStatus("Ungültige Kunden-ID."); } } + // --- Hilfsmethoden --- + + private boolean istLeer(JTextField f) { + return f.getText() == null || f.getText().trim().isEmpty(); + } + + private String leerAlsNull(JTextField f) { + String s = f.getText() == null ? null : f.getText().trim(); + return (s == null || s.isEmpty()) ? null : s; + } + private void clearFehler() { - lblFirmenfehler.setText(""); + lblNameFehler.setText(""); lblStrasseFehler.setText(""); lblPlzFehler.setText(""); lblOrtFehler.setText(""); - lblEmailFehler.setText("▲ Format: name@domain.tld"); + lblEmailFehler.setText("ⓘ Format: name@domain.tld"); lblEmailFehler.setForeground(new Color(120, 120, 120)); } @@ -337,15 +411,10 @@ public class KundenPanel extends JPanel { return lbl; } - private DokumentRepository createDummyDokumentRepository() { - return new DokumentRepository() { - public boolean existsReferencedProdukt(long id) { return false; } - public boolean existsReferencedKunde(long id) { return false; } - public int findMaxLaufendeNummer(String p, int j) { return 0; } - public void save(de.hsmannheim.faktura.dokumente.domain.Dokument d) {} - public java.util.Optional findByNummer(String n) { return java.util.Optional.empty(); } - public List findByKunde(long id) { return List.of(); } - public boolean existsFolgedokument(String q) { return false; } - }; + private JTextField createReadOnlyField() { + JTextField f = new JTextField(); + f.setEditable(false); + f.setBackground(new Color(230, 230, 230)); + return f; } }