Observer Patter, Flatlaf, Persistente Daten
parent
082fee2923
commit
26c3840d07
7
pom.xml
7
pom.xml
|
|
@ -44,6 +44,13 @@
|
|||
<version>${pdfbox.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Modernes Swing-Look-and-Feel (Q-05: Bedienbarkeit) -->
|
||||
<dependency>
|
||||
<groupId>com.formdev</groupId>
|
||||
<artifactId>flatlaf</artifactId>
|
||||
<version>3.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Modultests (JUnit 5, Kapitel 10 der Pflichtenhefte) -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import de.team1.faktura.dokumente.EinfacherBelegnummernGenerator;
|
|||
import de.team1.faktura.dokumente.JsonDokumentRepository;
|
||||
import de.team1.faktura.dokumente.PdfBoxPdfExporter;
|
||||
import de.team1.faktura.dokumente.StandardDokumentService;
|
||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||
import de.team1.faktura.gui.DokumentListenPanel;
|
||||
import de.team1.faktura.gui.HauptFenster;
|
||||
import de.team1.faktura.gui.KundenPanel;
|
||||
|
|
@ -20,6 +21,8 @@ import de.team1.faktura.produkte.JsonProduktRepository;
|
|||
import de.team1.faktura.produkte.ProduktCsvExport;
|
||||
import de.team1.faktura.produkte.ProduktVerwaltungsService;
|
||||
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.UIManager;
|
||||
import java.nio.file.Path;
|
||||
|
|
@ -49,17 +52,22 @@ public final class Main {
|
|||
// Gruppe A stellt die Referenzprüfungen für die Löschsperren bereit
|
||||
DokumentReferenzPruefung referenzPruefung = new DokumentReferenzPruefung(dokumentRepository);
|
||||
|
||||
// Observer-Verteiler: Services melden Datenänderungen, Panels abonnieren
|
||||
EreignisBus ereignisBus = new EreignisBus();
|
||||
|
||||
// Gruppe C — Kundenverwaltung
|
||||
KundenVerwaltungsService kundenService = new KundenVerwaltungsService(
|
||||
kundenRepository,
|
||||
EinfacherKundennummernGenerator.ausRepository(kundenRepository),
|
||||
referenzPruefung);
|
||||
referenzPruefung,
|
||||
ereignisBus);
|
||||
|
||||
// Gruppe B — Produktverwaltung
|
||||
ProduktVerwaltungsService produktService = new ProduktVerwaltungsService(
|
||||
produktRepository,
|
||||
EinfacherProduktnummernGenerator.ausRepository(produktRepository),
|
||||
referenzPruefung);
|
||||
referenzPruefung,
|
||||
ereignisBus);
|
||||
|
||||
// Gruppe A — Dokumentenzyklus
|
||||
DokumentService dokumentService = new StandardDokumentService(
|
||||
|
|
@ -67,20 +75,25 @@ public final class Main {
|
|||
EinfacherBelegnummernGenerator.ausRepository(dokumentRepository),
|
||||
kundenService,
|
||||
produktService,
|
||||
new PdfBoxPdfExporter());
|
||||
new PdfBoxPdfExporter(),
|
||||
ereignisBus);
|
||||
|
||||
// Gruppe D — Programmoberfläche
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
FlatLightLaf.setup();
|
||||
UIManager.put("Table.alternateRowColor", new java.awt.Color(245, 246, 248));
|
||||
UIManager.put("Table.showHorizontalLines", false);
|
||||
} catch (Exception e) {
|
||||
// Standard-Look-and-Feel verwenden
|
||||
}
|
||||
HauptFenster fenster = new HauptFenster(
|
||||
new KundenPanel(kundenService, new KundenCsvExport(kundenRepository)),
|
||||
new ProduktPanel(produktService, new ProduktCsvExport(produktRepository)),
|
||||
new KundenPanel(kundenService, new KundenCsvExport(kundenRepository),
|
||||
ereignisBus),
|
||||
new ProduktPanel(produktService, new ProduktCsvExport(produktRepository),
|
||||
ereignisBus),
|
||||
new DokumentListenPanel(dokumentService, kundenService, produktService,
|
||||
new DokumentCsvExport(dokumentRepository)));
|
||||
new DokumentCsvExport(dokumentRepository), ereignisBus));
|
||||
fenster.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,12 +40,10 @@ public class JsonDokumentRepository implements DokumentRepository {
|
|||
|
||||
private void schreibe() {
|
||||
try {
|
||||
if (datei.getParent() != null) {
|
||||
Files.createDirectories(datei.getParent());
|
||||
}
|
||||
// Über den Basistyp schreiben, damit die polymorphe Typ-ID ('typ')
|
||||
// erhalten bleibt und Belege beim Neustart wieder geladen werden (IF-01).
|
||||
mapper.writerFor(new TypeReference<List<Dokument>>() { }).writeValue(datei.toFile(), dokumente);
|
||||
JsonPersistenz.schreibeAtomar(datei,
|
||||
mapper.writerFor(new TypeReference<List<Dokument>>() { }), dokumente);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Belegbestand konnte nicht gespeichert werden: " + datei, e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package de.team1.faktura.dokumente;
|
||||
|
||||
import de.team1.faktura.gemeinsam.DatenBereich;
|
||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
import de.team1.faktura.kunden.Kunde;
|
||||
import de.team1.faktura.kunden.KundenService;
|
||||
|
|
@ -32,17 +34,29 @@ public class StandardDokumentService implements DokumentService {
|
|||
private final KundenService kundenService;
|
||||
private final ProduktService produktService;
|
||||
private final PdfExporter pdfExporter;
|
||||
private final EreignisBus ereignisBus;
|
||||
|
||||
public StandardDokumentService(DokumentRepository repository,
|
||||
BelegnummernGenerator nummernGenerator,
|
||||
KundenService kundenService,
|
||||
ProduktService produktService,
|
||||
PdfExporter pdfExporter) {
|
||||
this(repository, nummernGenerator, kundenService, produktService, pdfExporter,
|
||||
new EreignisBus());
|
||||
}
|
||||
|
||||
public StandardDokumentService(DokumentRepository repository,
|
||||
BelegnummernGenerator nummernGenerator,
|
||||
KundenService kundenService,
|
||||
ProduktService produktService,
|
||||
PdfExporter pdfExporter,
|
||||
EreignisBus ereignisBus) {
|
||||
this.repository = repository;
|
||||
this.nummernGenerator = nummernGenerator;
|
||||
this.kundenService = kundenService;
|
||||
this.produktService = produktService;
|
||||
this.pdfExporter = pdfExporter;
|
||||
this.ereignisBus = ereignisBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -58,6 +72,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
angebot.setGueltigBis(gueltigBis != null ? gueltigBis : datum.plusDays(STANDARD_GUELTIGKEIT_TAGE));
|
||||
angebot.setzePositionen(dokumentpositionen);
|
||||
repository.speichere(angebot);
|
||||
ereignisBus.melde(DatenBereich.DOKUMENTE);
|
||||
return angebot;
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +88,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
ab.setzeKunde(kunde.getKundennummer(), kunde.getName(), kunde.anschrift());
|
||||
ab.setzePositionen(dokumentpositionen);
|
||||
repository.speichere(ab);
|
||||
ereignisBus.melde(DatenBereich.DOKUMENTE);
|
||||
return ab;
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +105,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
lieferschein.setLieferdatum(lieferdatum != null ? lieferdatum : datum);
|
||||
lieferschein.setzePositionen(dokumentpositionen);
|
||||
repository.speichere(lieferschein);
|
||||
ereignisBus.melde(DatenBereich.DOKUMENTE);
|
||||
return lieferschein;
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +130,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
rechnung.setzePositionen(dokumentpositionen);
|
||||
rechnung.setzeStatus(DokumentStatus.OFFEN);
|
||||
repository.speichere(rechnung);
|
||||
ereignisBus.melde(DatenBereich.DOKUMENTE);
|
||||
return rechnung;
|
||||
}
|
||||
|
||||
|
|
@ -150,6 +168,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
rechnung.setzeStatus(DokumentStatus.OFFEN);
|
||||
}
|
||||
repository.speichere(folgebeleg);
|
||||
ereignisBus.melde(DatenBereich.DOKUMENTE);
|
||||
return folgebeleg;
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +177,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
Dokument dokument = pruefeBeleg(belegnummer);
|
||||
dokument.versende();
|
||||
repository.speichere(dokument);
|
||||
ereignisBus.melde(DatenBereich.DOKUMENTE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -169,6 +189,7 @@ public class StandardDokumentService implements DokumentService {
|
|||
}
|
||||
rechnung.storniere(LocalDate.now(), SYSTEM_BENUTZER);
|
||||
repository.speichere(rechnung);
|
||||
ereignisBus.melde(DatenBereich.DOKUMENTE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package de.team1.faktura.gemeinsam;
|
||||
|
||||
/**
|
||||
* Fachliche Datenbereiche der Anwendung, über die der {@link EreignisBus}
|
||||
* Änderungen meldet.
|
||||
*/
|
||||
public enum DatenBereich {
|
||||
KUNDEN,
|
||||
PRODUKTE,
|
||||
DOKUMENTE
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package de.team1.faktura.gemeinsam;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Einfacher synchroner Ereignis-Verteiler (Observer-Muster): Die Services
|
||||
* melden nach jeder schreibenden Operation den geänderten {@link DatenBereich},
|
||||
* die Modulansichten abonnieren die für sie relevanten Bereiche und
|
||||
* aktualisieren sich selbst. Dadurch entfallen manuelle Refresh-Aufrufe
|
||||
* zwischen den Modulen.
|
||||
*
|
||||
* <p>Alle Aufrufe laufen auf dem Event-Dispatch-Thread der Swing-Oberfläche;
|
||||
* eine Synchronisierung ist daher nicht erforderlich (Einzelplatzbetrieb).
|
||||
*/
|
||||
public class EreignisBus {
|
||||
|
||||
private final Map<DatenBereich, List<Runnable>> abonnenten = new EnumMap<>(DatenBereich.class);
|
||||
|
||||
/** Registriert einen Beobachter für Änderungen im angegebenen Bereich. */
|
||||
public void abonniere(DatenBereich bereich, Runnable beobachter) {
|
||||
abonnenten.computeIfAbsent(bereich, b -> new ArrayList<>()).add(beobachter);
|
||||
}
|
||||
|
||||
/** Meldet eine Änderung im angegebenen Bereich an alle Beobachter. */
|
||||
public void melde(DatenBereich bereich) {
|
||||
for (Runnable beobachter : abonnenten.getOrDefault(bereich, List.of())) {
|
||||
beobachter.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,16 @@ package de.team1.faktura.gemeinsam;
|
|||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
/**
|
||||
* Zentral konfigurierter Jackson-ObjectMapper für die lokale
|
||||
* JSON-Persistenz (IF-01). Datumswerte werden als ISO-Strings
|
||||
|
|
@ -23,4 +30,25 @@ public final class JsonPersistenz {
|
|||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt den Bestand atomar: erst vollständig in eine temporäre Datei,
|
||||
* dann per Move ersetzen. Ein Absturz während des Schreibens kann so den
|
||||
* vorhandenen Bestand nicht korrumpieren (GR-01/GR-02: der Belegbestand
|
||||
* ist Grundlage der lückenlosen Nummernvergabe und Unveränderlichkeit).
|
||||
*/
|
||||
public static void schreibeAtomar(Path datei, ObjectWriter writer, Object daten)
|
||||
throws IOException {
|
||||
if (datei.getParent() != null) {
|
||||
Files.createDirectories(datei.getParent());
|
||||
}
|
||||
Path temp = datei.resolveSibling(datei.getFileName() + ".tmp");
|
||||
writer.writeValue(temp.toFile(), daten);
|
||||
try {
|
||||
Files.move(temp, datei, StandardCopyOption.REPLACE_EXISTING,
|
||||
StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(temp, datei, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,17 @@ import de.team1.faktura.dokumente.Dokument;
|
|||
import de.team1.faktura.dokumente.DokumentCsvExport;
|
||||
import de.team1.faktura.dokumente.DokumentService;
|
||||
import de.team1.faktura.dokumente.DokumentStatus;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
import de.team1.faktura.gemeinsam.DatenBereich;
|
||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||
import de.team1.faktura.kunden.KundenService;
|
||||
import de.team1.faktura.produkte.ProduktService;
|
||||
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
|
|
@ -20,6 +23,7 @@ import javax.swing.ListSelectionModel;
|
|||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Window;
|
||||
|
|
@ -48,8 +52,9 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
private final DokumentCsvExport datenExport;
|
||||
private final DokumentListenController controller;
|
||||
|
||||
private final JComboBox<String> statusFilter = new JComboBox<>(
|
||||
new String[]{"Alle", "ENTWURF", "OFFEN", "VERSENDET", "STORNIERT"});
|
||||
/** Statusfilter; {@code null} steht für "Alle" (F-06). */
|
||||
private final JComboBox<DokumentStatus> statusFilter = new JComboBox<>(
|
||||
statusFilterWerte());
|
||||
private final DokumentTabellenModel tabellenModel = new DokumentTabellenModel();
|
||||
private final JTable tabelle = new JTable(tabellenModel);
|
||||
|
||||
|
|
@ -63,7 +68,8 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
public DokumentListenPanel(DokumentService dokumentService,
|
||||
KundenService kundenService,
|
||||
ProduktService produktService,
|
||||
DokumentCsvExport datenExport) {
|
||||
DokumentCsvExport datenExport,
|
||||
EreignisBus ereignisBus) {
|
||||
this.dokumentService = dokumentService;
|
||||
this.kundenService = kundenService;
|
||||
this.produktService = produktService;
|
||||
|
|
@ -71,6 +77,9 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
this.controller = new DokumentListenController(dokumentService);
|
||||
baueOberflaeche();
|
||||
aktualisiere();
|
||||
ereignisBus.abonniere(DatenBereich.DOKUMENTE, this::aktualisiere);
|
||||
// Kundenname und -nummer werden in der Belegliste angezeigt
|
||||
ereignisBus.abonniere(DatenBereich.KUNDEN, this::aktualisiere);
|
||||
}
|
||||
|
||||
private void baueOberflaeche() {
|
||||
|
|
@ -79,6 +88,14 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
JPanel kopf = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
kopf.add(new JLabel("Statusfilter:"));
|
||||
kopf.add(statusFilter);
|
||||
statusFilter.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> liste, Object wert,
|
||||
int index, boolean selektiert, boolean fokus) {
|
||||
return super.getListCellRendererComponent(liste,
|
||||
wert == null ? "Alle" : wert, index, selektiert, fokus);
|
||||
}
|
||||
});
|
||||
statusFilter.addActionListener(e -> aktualisiere());
|
||||
|
||||
JButton neueRechnung = new JButton("Neue Rechnung (Assistent)…");
|
||||
|
|
@ -94,6 +111,9 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
|
||||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
tabelle.getSelectionModel().addListSelectionListener(e -> aktualisiereAktionen());
|
||||
TabellenFormat.konfiguriere(tabelle);
|
||||
tabelle.getColumnModel().getColumn(4).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
||||
tabelle.getColumnModel().getColumn(5).setCellRenderer(TabellenFormat.statusRenderer());
|
||||
add(new JScrollPane(tabelle), BorderLayout.CENTER);
|
||||
|
||||
JPanel aktionen = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
|
|
@ -115,7 +135,8 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
|
||||
private Dokument auswahl() {
|
||||
int zeile = tabelle.getSelectedRow();
|
||||
return zeile < 0 ? null : tabellenModel.dokumente.get(zeile);
|
||||
return zeile < 0 ? null
|
||||
: tabellenModel.dokumente.get(tabelle.convertRowIndexToModel(zeile));
|
||||
}
|
||||
|
||||
/** Aktiviert/deaktiviert die Belegaktionen gemäß Status (F-08, F-14). */
|
||||
|
|
@ -144,14 +165,12 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
RechnungsWizardDialog dialog = new RechnungsWizardDialog(fenster, wizardController,
|
||||
kundenService, produktService);
|
||||
dialog.setVisible(true);
|
||||
aktualisiere();
|
||||
}
|
||||
|
||||
private void oeffneBelegDialog() {
|
||||
Window fenster = SwingUtilities.getWindowAncestor(this);
|
||||
BelegDialog dialog = new BelegDialog(fenster, dokumentService, kundenService, produktService);
|
||||
dialog.setVisible(true);
|
||||
aktualisiere();
|
||||
}
|
||||
|
||||
private void erzeugeFolgebeleg() {
|
||||
|
|
@ -159,15 +178,12 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
if (dokument == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
Dokument folgebeleg = dokumentService.erzeugeFolgebeleg(dokument.getBelegnummer());
|
||||
aktualisiere();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg(folgebeleg.belegtyp().anzeigename() + " "
|
||||
+ folgebeleg.getBelegnummer() + " wurde aus " + dokument.getBelegnummer()
|
||||
+ " erzeugt."), null);
|
||||
} catch (ValidierungsException e) {
|
||||
MeldungsAnzeige.zeige(this, Meldung.fehler(e.getFeldname(), e.getMessage()), null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void versende() {
|
||||
|
|
@ -182,14 +198,11 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
dokumentService.versende(dokument.getBelegnummer());
|
||||
aktualisiere();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Beleg " + dokument.getBelegnummer()
|
||||
+ " ist jetzt im Status VERSENDET."), null);
|
||||
} catch (IllegalStateException e) {
|
||||
MeldungsAnzeige.zeige(this, Meldung.fehler(null, e.getMessage()), null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Stornierung mit Bestätigungsdialog: Rechnungsnummer und Bruttosumme (F-15). */
|
||||
|
|
@ -204,7 +217,6 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
"Rechnung stornieren", JOptionPane.YES_NO_OPTION);
|
||||
Meldung meldung = controller.storniere(dokument.getBelegnummer(),
|
||||
antwort == JOptionPane.YES_OPTION);
|
||||
aktualisiere();
|
||||
MeldungsAnzeige.zeige(this, meldung, null);
|
||||
}
|
||||
|
||||
|
|
@ -277,17 +289,18 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
|
||||
@Override
|
||||
public void aktualisiere() {
|
||||
DokumentStatus filter = switch ((String) statusFilter.getSelectedItem()) {
|
||||
case "ENTWURF" -> DokumentStatus.ENTWURF;
|
||||
case "OFFEN" -> DokumentStatus.OFFEN;
|
||||
case "VERSENDET" -> DokumentStatus.VERSENDET;
|
||||
case "STORNIERT" -> DokumentStatus.STORNIERT;
|
||||
default -> null;
|
||||
};
|
||||
tabellenModel.setze(controller.gefiltert(filter));
|
||||
tabellenModel.setze(controller.gefiltert((DokumentStatus) statusFilter.getSelectedItem()));
|
||||
aktualisiereAktionen();
|
||||
}
|
||||
|
||||
/** "Alle" (= {@code null}) gefolgt von allen Belegstatus. */
|
||||
private static DokumentStatus[] statusFilterWerte() {
|
||||
DokumentStatus[] status = DokumentStatus.values();
|
||||
DokumentStatus[] werte = new DokumentStatus[status.length + 1];
|
||||
System.arraycopy(status, 0, werte, 1, status.length);
|
||||
return werte;
|
||||
}
|
||||
|
||||
private static final class DokumentTabellenModel extends AbstractTableModel {
|
||||
|
||||
private static final String[] SPALTEN =
|
||||
|
|
@ -315,6 +328,15 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
return SPALTEN[spalte];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int spalte) {
|
||||
return switch (spalte) {
|
||||
case 4 -> java.math.BigDecimal.class;
|
||||
case 5 -> DokumentStatus.class;
|
||||
default -> String.class;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int zeile, int spalte) {
|
||||
Dokument dokument = dokumente.get(zeile);
|
||||
|
|
@ -323,8 +345,8 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
case 1 -> dokument.belegtyp().anzeigename();
|
||||
case 2 -> dokument.getDatum() == null ? "" : DATUM.format(dokument.getDatum());
|
||||
case 3 -> dokument.getKundeName() + " (" + dokument.getKundenReferenz() + ")";
|
||||
case 4 -> dokument.getSummeBrutto().toPlainString() + " EUR";
|
||||
default -> dokument.getStatus().name();
|
||||
case 4 -> dokument.getSummeBrutto();
|
||||
default -> dokument.getStatus();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.gemeinsam.LoeschAbgelehntException;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
import de.team1.faktura.gemeinsam.DatenBereich;
|
||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||
import de.team1.faktura.kunden.Kunde;
|
||||
import de.team1.faktura.kunden.KundenCsvExport;
|
||||
import de.team1.faktura.kunden.KundenVerwaltungsService;
|
||||
|
|
@ -56,7 +56,8 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
private String gewaehlteNummer;
|
||||
private boolean ungespeichert;
|
||||
|
||||
public KundenPanel(KundenVerwaltungsService service, KundenCsvExport csvExport) {
|
||||
public KundenPanel(KundenVerwaltungsService service, KundenCsvExport csvExport,
|
||||
EreignisBus ereignisBus) {
|
||||
this.service = service;
|
||||
this.csvExport = csvExport;
|
||||
felder.put("Name", nameFeld);
|
||||
|
|
@ -66,6 +67,7 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
felder.put("E-Mail", eMailFeld);
|
||||
baueOberflaeche();
|
||||
aktualisiere();
|
||||
ereignisBus.abonniere(DatenBereich.KUNDEN, this::aktualisiere);
|
||||
}
|
||||
|
||||
private void baueOberflaeche() {
|
||||
|
|
@ -78,6 +80,7 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
add(suchleiste, BorderLayout.NORTH);
|
||||
|
||||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
TabellenFormat.konfiguriere(tabelle);
|
||||
tabelle.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) {
|
||||
ladeAuswahl();
|
||||
|
|
@ -148,7 +151,7 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
}
|
||||
|
||||
private void speichere() {
|
||||
try {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
Kunde kunde = gewaehlteNummer == null
|
||||
? new Kunde()
|
||||
: service.findeKunde(gewaehlteNummer);
|
||||
|
|
@ -166,12 +169,9 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
ungespeichert = false;
|
||||
gewaehlteNummer = gespeichert.getKundennummer();
|
||||
nummerAnzeige.setText(gespeichert.getKundennummer());
|
||||
aktualisiere();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gespeichert. Kundennummer: "
|
||||
+ gespeichert.getKundennummer()), felder);
|
||||
} catch (ValidierungsException e) {
|
||||
MeldungsAnzeige.zeige(this, Meldung.fehler(e.getFeldname(), e.getMessage()), felder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loesche() {
|
||||
|
|
@ -184,14 +184,11 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
service.loescheKunde(gewaehlteNummer);
|
||||
leereFormular();
|
||||
aktualisiere();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gelöscht."), felder);
|
||||
} catch (LoeschAbgelehntException e) {
|
||||
MeldungsAnzeige.zeige(this, Meldung.fehler(null, e.getMessage()), felder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void exportiere() {
|
||||
|
|
@ -209,7 +206,7 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
if (zeile < 0) {
|
||||
return;
|
||||
}
|
||||
Kunde kunde = tabellenModel.kunden.get(zeile);
|
||||
Kunde kunde = tabellenModel.kunden.get(tabelle.convertRowIndexToModel(zeile));
|
||||
gewaehlteNummer = kunde.getKundennummer();
|
||||
nummerAnzeige.setText(kunde.getKundennummer());
|
||||
nameFeld.setText(kunde.getName());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.gemeinsam.LoeschAbgelehntException;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JOptionPane;
|
||||
|
|
@ -7,6 +10,7 @@ import javax.swing.UIManager;
|
|||
import javax.swing.border.Border;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
|
|
@ -48,4 +52,24 @@ public final class MeldungsAnzeige {
|
|||
JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zentrale Fehlerbehandlung der Modulansichten: führt die Aktion aus und
|
||||
* zeigt fachliche Fehler einheitlich an — Validierungsfehler mit
|
||||
* Feldmarkierung (Q-09), abgelehnte Löschvorgänge (GR-04), unzulässige
|
||||
* Statuswechsel (GR-02) und Persistenzfehler beim Speichern (IF-01).
|
||||
*/
|
||||
public static void mitFehlerbehandlung(Component parent, Map<String, JComponent> felder,
|
||||
Runnable aktion) {
|
||||
try {
|
||||
aktion.run();
|
||||
} catch (ValidierungsException e) {
|
||||
zeige(parent, Meldung.fehler(e.getFeldname(), e.getMessage()), felder);
|
||||
} catch (LoeschAbgelehntException | IllegalStateException e) {
|
||||
zeige(parent, Meldung.fehler(null, e.getMessage()), felder);
|
||||
} catch (UncheckedIOException e) {
|
||||
zeige(parent, Meldung.fehler(null,
|
||||
"Die Daten konnten nicht gespeichert werden: " + e.getMessage()), felder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.gemeinsam.LoeschAbgelehntException;
|
||||
import de.team1.faktura.gemeinsam.DatenBereich;
|
||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
import de.team1.faktura.produkte.Produkt;
|
||||
import de.team1.faktura.produkte.ProduktCsvExport;
|
||||
|
|
@ -58,7 +59,8 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
private String gewaehlteNummer;
|
||||
private boolean ungespeichert;
|
||||
|
||||
public ProduktPanel(ProduktVerwaltungsService service, ProduktCsvExport csvExport) {
|
||||
public ProduktPanel(ProduktVerwaltungsService service, ProduktCsvExport csvExport,
|
||||
EreignisBus ereignisBus) {
|
||||
this.service = service;
|
||||
this.csvExport = csvExport;
|
||||
felder.put("Bezeichnung", bezeichnungFeld);
|
||||
|
|
@ -66,6 +68,7 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
felder.put("Steuersatz", steuersatzWahl);
|
||||
baueOberflaeche();
|
||||
aktualisiere();
|
||||
ereignisBus.abonniere(DatenBereich.PRODUKTE, this::aktualisiere);
|
||||
}
|
||||
|
||||
private void baueOberflaeche() {
|
||||
|
|
@ -78,6 +81,8 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
add(suchleiste, BorderLayout.NORTH);
|
||||
|
||||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
TabellenFormat.konfiguriere(tabelle);
|
||||
tabelle.getColumnModel().getColumn(2).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
||||
tabelle.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) {
|
||||
ladeAuswahl();
|
||||
|
|
@ -145,7 +150,7 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
}
|
||||
|
||||
private void speichere() {
|
||||
try {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
Produkt produkt = gewaehlteNummer == null
|
||||
? new Produkt()
|
||||
: service.findeProdukt(gewaehlteNummer);
|
||||
|
|
@ -161,12 +166,9 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
ungespeichert = false;
|
||||
gewaehlteNummer = gespeichert.getProduktnummer();
|
||||
nummerAnzeige.setText(gespeichert.getProduktnummer());
|
||||
aktualisiere();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gespeichert. Produktnummer: "
|
||||
+ gespeichert.getProduktnummer()), felder);
|
||||
} catch (ValidierungsException e) {
|
||||
MeldungsAnzeige.zeige(this, Meldung.fehler(e.getFeldname(), e.getMessage()), felder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Akzeptiert deutsches und englisches Dezimaltrennzeichen. */
|
||||
|
|
@ -202,14 +204,11 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
service.loescheProdukt(gewaehlteNummer);
|
||||
leereFormular();
|
||||
aktualisiere();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gelöscht."), felder);
|
||||
} catch (LoeschAbgelehntException e) {
|
||||
MeldungsAnzeige.zeige(this, Meldung.fehler(null, e.getMessage()), felder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void exportiere() {
|
||||
|
|
@ -227,7 +226,7 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
if (zeile < 0) {
|
||||
return;
|
||||
}
|
||||
Produkt produkt = tabellenModel.produkte.get(zeile);
|
||||
Produkt produkt = tabellenModel.produkte.get(tabelle.convertRowIndexToModel(zeile));
|
||||
gewaehlteNummer = produkt.getProduktnummer();
|
||||
nummerAnzeige.setText(produkt.getProduktnummer());
|
||||
bezeichnungFeld.setText(produkt.getBezeichnung());
|
||||
|
|
@ -335,13 +334,18 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
return SPALTEN[spalte];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int spalte) {
|
||||
return spalte == 2 ? BigDecimal.class : String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int zeile, int spalte) {
|
||||
Produkt produkt = produkte.get(zeile);
|
||||
return switch (spalte) {
|
||||
case 0 -> produkt.getProduktnummer();
|
||||
case 1 -> produkt.getBezeichnung();
|
||||
case 2 -> produkt.getEinzelpreisNetto().toPlainString() + " EUR";
|
||||
case 2 -> produkt.getEinzelpreisNetto();
|
||||
case 3 -> produkt.getSteuersatz().multiply(new BigDecimal("100"))
|
||||
.stripTrailingZeros().toPlainString() + " %";
|
||||
default -> produkt.getEinheit() == null ? "" : produkt.getEinheit();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.dokumente.DokumentStatus;
|
||||
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.math.BigDecimal;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Gemeinsame Tabellendarstellung der Modulansichten (Q-05): Währungsbeträge
|
||||
* rechtsbündig im deutschen Format, Belegstatus farblich hervorgehoben,
|
||||
* einheitliche Zeilenhöhe und Sortierung per Spaltenkopf.
|
||||
*/
|
||||
public final class TabellenFormat {
|
||||
|
||||
private TabellenFormat() {
|
||||
}
|
||||
|
||||
/** Formatiert einen Betrag im deutschen Währungsformat, z. B. "1.234,50 €". */
|
||||
public static String formatiereBetrag(BigDecimal betrag) {
|
||||
return NumberFormat.getCurrencyInstance(Locale.GERMANY).format(betrag);
|
||||
}
|
||||
|
||||
/** Einheitliche Grundeinstellungen: Zeilenhöhe und Sortierung per Spaltenkopf. */
|
||||
public static void konfiguriere(JTable tabelle) {
|
||||
tabelle.setRowHeight(24);
|
||||
tabelle.setAutoCreateRowSorter(true);
|
||||
tabelle.setFillsViewportHeight(true);
|
||||
}
|
||||
|
||||
/** Rechtsbündiger Renderer für {@link BigDecimal}-Beträge im Währungsformat. */
|
||||
public static DefaultTableCellRenderer waehrungsRenderer() {
|
||||
DefaultTableCellRenderer renderer = new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
protected void setValue(Object wert) {
|
||||
setText(wert instanceof BigDecimal betrag ? formatiereBetrag(betrag) : "");
|
||||
}
|
||||
};
|
||||
renderer.setHorizontalAlignment(JLabel.RIGHT);
|
||||
return renderer;
|
||||
}
|
||||
|
||||
/** Renderer für {@link DokumentStatus}: dezente Statusfarbe, Selektion bleibt lesbar. */
|
||||
public static DefaultTableCellRenderer statusRenderer() {
|
||||
return new DefaultTableCellRenderer() {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable tabelle, Object wert,
|
||||
boolean selektiert, boolean fokus, int zeile, int spalte) {
|
||||
super.getTableCellRendererComponent(tabelle, wert, selektiert, fokus, zeile, spalte);
|
||||
if (wert instanceof DokumentStatus status) {
|
||||
setText(status.name());
|
||||
setForeground(selektiert ? tabelle.getSelectionForeground() : farbeFuer(status));
|
||||
} else {
|
||||
setForeground(selektiert ? tabelle.getSelectionForeground()
|
||||
: tabelle.getForeground());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Color farbeFuer(DokumentStatus status) {
|
||||
return switch (status) {
|
||||
case ENTWURF -> new Color(110, 110, 110);
|
||||
case OFFEN -> new Color(0, 90, 180);
|
||||
case VERSENDET -> new Color(0, 130, 60);
|
||||
case STORNIERT -> new Color(180, 40, 40);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -42,10 +42,7 @@ public class JsonKundenRepository implements KundenRepository {
|
|||
|
||||
private void schreibe() {
|
||||
try {
|
||||
if (datei.getParent() != null) {
|
||||
Files.createDirectories(datei.getParent());
|
||||
}
|
||||
mapper.writeValue(datei.toFile(), kunden);
|
||||
JsonPersistenz.schreibeAtomar(datei, mapper.writer(), kunden);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Kundenbestand konnte nicht gespeichert werden: " + datei, e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package de.team1.faktura.kunden;
|
||||
|
||||
import de.team1.faktura.gemeinsam.DatenBereich;
|
||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||
import de.team1.faktura.gemeinsam.LoeschAbgelehntException;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
|
||||
|
|
@ -15,20 +17,31 @@ public class KundenVerwaltungsService implements KundenService {
|
|||
private final KundenRepository repository;
|
||||
private final KundennummernGenerator nummernGenerator;
|
||||
private final KundenReferenzPruefung referenzPruefung;
|
||||
private final EreignisBus ereignisBus;
|
||||
|
||||
public KundenVerwaltungsService(KundenRepository repository,
|
||||
KundennummernGenerator nummernGenerator,
|
||||
KundenReferenzPruefung referenzPruefung) {
|
||||
this(repository, nummernGenerator, referenzPruefung, new EreignisBus());
|
||||
}
|
||||
|
||||
public KundenVerwaltungsService(KundenRepository repository,
|
||||
KundennummernGenerator nummernGenerator,
|
||||
KundenReferenzPruefung referenzPruefung,
|
||||
EreignisBus ereignisBus) {
|
||||
this.repository = repository;
|
||||
this.nummernGenerator = nummernGenerator;
|
||||
this.referenzPruefung = referenzPruefung;
|
||||
this.ereignisBus = ereignisBus;
|
||||
}
|
||||
|
||||
/** Legt einen neuen Kunden an und vergibt die Kundennummer (F-01, F-02). */
|
||||
public Kunde legeAn(Kunde kunde) {
|
||||
validiere(kunde);
|
||||
kunde.setKundennummer(nummernGenerator.naechsteNummer());
|
||||
return repository.speichere(kunde);
|
||||
Kunde gespeichert = repository.speichere(kunde);
|
||||
ereignisBus.melde(DatenBereich.KUNDEN);
|
||||
return gespeichert;
|
||||
}
|
||||
|
||||
/** Ändert einen bestehenden Kunden; die Pflichtfeldprüfung gilt unverändert (F-05). */
|
||||
|
|
@ -37,7 +50,9 @@ public class KundenVerwaltungsService implements KundenService {
|
|||
throw new ValidierungsException("Kundennummer", "Der Kunde wurde noch nicht angelegt.");
|
||||
}
|
||||
validiere(kunde);
|
||||
return repository.speichere(kunde);
|
||||
Kunde gespeichert = repository.speichere(kunde);
|
||||
ereignisBus.melde(DatenBereich.KUNDEN);
|
||||
return gespeichert;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -52,6 +67,7 @@ public class KundenVerwaltungsService implements KundenService {
|
|||
+ anzahl + " verknüpfte Dokumente vorhanden (GR-04).");
|
||||
}
|
||||
repository.loesche(kundennummer);
|
||||
ereignisBus.melde(DatenBereich.KUNDEN);
|
||||
}
|
||||
|
||||
public List<Kunde> alleSortiertNachName() {
|
||||
|
|
|
|||
|
|
@ -42,10 +42,7 @@ public class JsonProduktRepository implements ProduktRepository {
|
|||
|
||||
private void schreibe() {
|
||||
try {
|
||||
if (datei.getParent() != null) {
|
||||
Files.createDirectories(datei.getParent());
|
||||
}
|
||||
mapper.writeValue(datei.toFile(), produkte);
|
||||
JsonPersistenz.schreibeAtomar(datei, mapper.writer(), produkte);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Produktbestand konnte nicht gespeichert werden: " + datei, e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package de.team1.faktura.produkte;
|
||||
|
||||
import de.team1.faktura.gemeinsam.DatenBereich;
|
||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||
import de.team1.faktura.gemeinsam.LoeschAbgelehntException;
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
|
||||
|
|
@ -20,20 +22,31 @@ public class ProduktVerwaltungsService implements ProduktService {
|
|||
private final ProduktRepository repository;
|
||||
private final ProduktnummernGenerator nummernGenerator;
|
||||
private final ProduktReferenzPruefung referenzPruefung;
|
||||
private final EreignisBus ereignisBus;
|
||||
|
||||
public ProduktVerwaltungsService(ProduktRepository repository,
|
||||
ProduktnummernGenerator nummernGenerator,
|
||||
ProduktReferenzPruefung referenzPruefung) {
|
||||
this(repository, nummernGenerator, referenzPruefung, new EreignisBus());
|
||||
}
|
||||
|
||||
public ProduktVerwaltungsService(ProduktRepository repository,
|
||||
ProduktnummernGenerator nummernGenerator,
|
||||
ProduktReferenzPruefung referenzPruefung,
|
||||
EreignisBus ereignisBus) {
|
||||
this.repository = repository;
|
||||
this.nummernGenerator = nummernGenerator;
|
||||
this.referenzPruefung = referenzPruefung;
|
||||
this.ereignisBus = ereignisBus;
|
||||
}
|
||||
|
||||
/** Legt ein neues Produkt an und vergibt die Produktnummer (F-01, F-02). */
|
||||
public Produkt legeAn(Produkt produkt) {
|
||||
validiere(produkt);
|
||||
produkt.setProduktnummer(nummernGenerator.naechsteNummer());
|
||||
return repository.speichere(produkt);
|
||||
Produkt gespeichert = repository.speichere(produkt);
|
||||
ereignisBus.melde(DatenBereich.PRODUKTE);
|
||||
return gespeichert;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +58,9 @@ public class ProduktVerwaltungsService implements ProduktService {
|
|||
throw new ValidierungsException("Produktnummer", "Das Produkt wurde noch nicht angelegt.");
|
||||
}
|
||||
validiere(produkt);
|
||||
return repository.speichere(produkt);
|
||||
Produkt gespeichert = repository.speichere(produkt);
|
||||
ereignisBus.melde(DatenBereich.PRODUKTE);
|
||||
return gespeichert;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,6 +74,7 @@ public class ProduktVerwaltungsService implements ProduktService {
|
|||
+ "es wird in Dokumenten verwendet.");
|
||||
}
|
||||
repository.loesche(produktnummer);
|
||||
ereignisBus.melde(DatenBereich.PRODUKTE);
|
||||
}
|
||||
|
||||
public List<Produkt> alleSortiertNachBezeichnung() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package de.team1.faktura.gemeinsam;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class EreignisBusTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("melde benachrichtigt alle Beobachter des Bereichs")
|
||||
void benachrichtigtAlleBeobachter() {
|
||||
EreignisBus bus = new EreignisBus();
|
||||
AtomicInteger ersterBeobachter = new AtomicInteger();
|
||||
AtomicInteger zweiterBeobachter = new AtomicInteger();
|
||||
bus.abonniere(DatenBereich.KUNDEN, ersterBeobachter::incrementAndGet);
|
||||
bus.abonniere(DatenBereich.KUNDEN, zweiterBeobachter::incrementAndGet);
|
||||
|
||||
bus.melde(DatenBereich.KUNDEN);
|
||||
bus.melde(DatenBereich.KUNDEN);
|
||||
|
||||
assertEquals(2, ersterBeobachter.get());
|
||||
assertEquals(2, zweiterBeobachter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("melde benachrichtigt nur Beobachter des betroffenen Bereichs")
|
||||
void benachrichtigtNurBetroffenenBereich() {
|
||||
EreignisBus bus = new EreignisBus();
|
||||
AtomicInteger kundenBeobachter = new AtomicInteger();
|
||||
AtomicInteger dokumentBeobachter = new AtomicInteger();
|
||||
bus.abonniere(DatenBereich.KUNDEN, kundenBeobachter::incrementAndGet);
|
||||
bus.abonniere(DatenBereich.DOKUMENTE, dokumentBeobachter::incrementAndGet);
|
||||
|
||||
bus.melde(DatenBereich.DOKUMENTE);
|
||||
|
||||
assertEquals(0, kundenBeobachter.get());
|
||||
assertEquals(1, dokumentBeobachter.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("melde ohne Beobachter ist wirkungslos und wirft nicht")
|
||||
void meldenOhneBeobachterIstWirkungslos() {
|
||||
new EreignisBus().melde(DatenBereich.PRODUKTE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package de.team1.faktura.gemeinsam;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JsonPersistenzTest {
|
||||
|
||||
private final ObjectMapper mapper = JsonPersistenz.mapper();
|
||||
|
||||
@Test
|
||||
@DisplayName("schreibeAtomar legt die Zieldatei an und lässt keine Temp-Datei zurück")
|
||||
void schreibtOhneTempDateiRueckstand(@TempDir Path verzeichnis) throws IOException {
|
||||
Path datei = verzeichnis.resolve("bestand.json");
|
||||
|
||||
JsonPersistenz.schreibeAtomar(datei, mapper.writer(), List.of("a", "b"));
|
||||
|
||||
assertTrue(Files.exists(datei));
|
||||
assertFalse(Files.exists(verzeichnis.resolve("bestand.json.tmp")),
|
||||
"Temp-Datei darf nach dem Move nicht zurückbleiben");
|
||||
List<?> gelesen = mapper.readValue(datei.toFile(), List.class);
|
||||
assertEquals(List.of("a", "b"), gelesen);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("schreibeAtomar ersetzt einen vorhandenen Bestand vollständig")
|
||||
void ersetztVorhandenenBestand(@TempDir Path verzeichnis) throws IOException {
|
||||
Path datei = verzeichnis.resolve("bestand.json");
|
||||
JsonPersistenz.schreibeAtomar(datei, mapper.writer(), List.of("alt"));
|
||||
|
||||
JsonPersistenz.schreibeAtomar(datei, mapper.writer(), List.of("neu1", "neu2"));
|
||||
|
||||
List<?> gelesen = mapper.readValue(datei.toFile(), List.class);
|
||||
assertEquals(List.of("neu1", "neu2"), gelesen);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("schreibeAtomar legt fehlende Elternverzeichnisse an")
|
||||
void legtElternverzeichnisseAn(@TempDir Path verzeichnis) throws IOException {
|
||||
Path datei = verzeichnis.resolve("unterordner").resolve("bestand.json");
|
||||
|
||||
JsonPersistenz.schreibeAtomar(datei, mapper.writer(), List.of("a"));
|
||||
|
||||
assertTrue(Files.exists(datei));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue