Update UI
parent
26c3840d07
commit
3846ba0cde
|
|
@ -12,6 +12,7 @@ import de.team1.faktura.gui.DokumentListenPanel;
|
|||
import de.team1.faktura.gui.HauptFenster;
|
||||
import de.team1.faktura.gui.KundenPanel;
|
||||
import de.team1.faktura.gui.ProduktPanel;
|
||||
import de.team1.faktura.gui.StammdatenController;
|
||||
import de.team1.faktura.kunden.EinfacherKundennummernGenerator;
|
||||
import de.team1.faktura.kunden.JsonKundenRepository;
|
||||
import de.team1.faktura.kunden.KundenCsvExport;
|
||||
|
|
@ -78,6 +79,10 @@ public final class Main {
|
|||
new PdfBoxPdfExporter(),
|
||||
ereignisBus);
|
||||
|
||||
// Gruppe D — GUI-freier Controller der Stammdaten-Ansichten (D-F-03)
|
||||
StammdatenController stammdatenController =
|
||||
new StammdatenController(kundenService, produktService);
|
||||
|
||||
// Gruppe D — Programmoberfläche
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
|
|
@ -88,10 +93,10 @@ public final class Main {
|
|||
// Standard-Look-and-Feel verwenden
|
||||
}
|
||||
HauptFenster fenster = new HauptFenster(
|
||||
new KundenPanel(kundenService, new KundenCsvExport(kundenRepository),
|
||||
ereignisBus),
|
||||
new ProduktPanel(produktService, new ProduktCsvExport(produktRepository),
|
||||
ereignisBus),
|
||||
new KundenPanel(kundenService, stammdatenController,
|
||||
new KundenCsvExport(kundenRepository), ereignisBus),
|
||||
new ProduktPanel(produktService, stammdatenController,
|
||||
new ProduktCsvExport(produktRepository), ereignisBus),
|
||||
new DokumentListenPanel(dokumentService, kundenService, produktService,
|
||||
new DokumentCsvExport(dokumentRepository), ereignisBus));
|
||||
fenster.setVisible(true);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,10 @@ public abstract class Dokument {
|
|||
BigDecimal netto = BigDecimal.ZERO;
|
||||
BigDecimal steuer = BigDecimal.ZERO;
|
||||
for (Dokumentposition position : positionen) {
|
||||
netto = netto.add(position.getPositionssummeNetto());
|
||||
// Altdaten ohne Preis-Snapshot (IF-01) zählen als 0 statt zu scheitern
|
||||
if (position.getPositionssummeNetto() != null) {
|
||||
netto = netto.add(position.getPositionssummeNetto());
|
||||
}
|
||||
steuer = steuer.add(position.getSteuerbetrag());
|
||||
}
|
||||
this.summeNetto = netto.setScale(2, RoundingMode.HALF_UP);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ public class Dokumentposition {
|
|||
|
||||
/** Steuerbetrag der Position: {@code positionssummeNetto * steuersatz}, Scale 2 (F-23, TC-01). */
|
||||
public BigDecimal getSteuerbetrag() {
|
||||
// Altdaten ohne Preis-Snapshot (IF-01) liefern 0 statt eines Fehlers
|
||||
if (positionssummeNetto == null || steuersatz == null) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
return positionssummeNetto.multiply(steuersatz).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,19 +13,48 @@ import java.io.UncheckedIOException;
|
|||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* PDF-Export der Belege mit Apache PDFBox (A-F-04, F-07, F-10, F-15).
|
||||
* Rechnungen enthalten die Pflichtangaben gemäß § 14 UStG (F-13):
|
||||
* Belegnummer, Rechnungs- und Leistungsdatum, Kundendaten, Positionen mit
|
||||
* Einzel- und Gesamtbeträgen, Steuersatz/-betrag sowie die Summen.
|
||||
* Standardisierter PDF-Export der Belege mit Apache PDFBox
|
||||
* (A-F-04, F-07, F-10, F-15), angelehnt an den deutschen Geschäftsbrief:
|
||||
* Absender- und Empfängerblock, Belegkopf mit Datum und Referenzen,
|
||||
* Positionstabelle mit festen Spalten und rechtsbündigen Beträgen sowie
|
||||
* Summenblock.
|
||||
*
|
||||
* <p>Rechnungen enthalten die Pflichtangaben gemäß § 14 UStG (F-13):
|
||||
* Name und Anschrift von Aussteller und Kunde, Belegnummer, Rechnungs- und
|
||||
* Leistungsdatum, Positionen mit Mengen und Einzelbeträgen, Steuersatz und
|
||||
* Steuerbetrag sowie Netto-/Bruttosummen.
|
||||
*/
|
||||
public class PdfBoxPdfExporter implements PdfExporter {
|
||||
|
||||
/** Aussteller-Stammdaten (§ 14 UStG); bei Produktivbetrieb anzupassen. */
|
||||
private static final String AUSSTELLER_NAME = "Team 1 Fakturierung";
|
||||
private static final String AUSSTELLER_STRASSE = "Musterstraße 1";
|
||||
private static final String AUSSTELLER_ORT = "68163 Mannheim";
|
||||
private static final String AUSSTELLER_UST_ID = "USt-IdNr. DE000000000";
|
||||
|
||||
private static final DateTimeFormatter DATUM = DateTimeFormatter.ofPattern("dd.MM.yyyy");
|
||||
|
||||
/** Seitenränder und Zeilenraster (A4 hoch, Angaben in PDF-Punkten). */
|
||||
private static final float RAND = 50;
|
||||
private static final float ZEILENHOEHE = 14;
|
||||
private static final float SEITENBREITE = PDRectangle.A4.getWidth();
|
||||
private static final float RECHTS = SEITENBREITE - RAND;
|
||||
|
||||
/** Spaltenraster der Positionstabelle: Textspalten linksbündig ... */
|
||||
private static final float SPALTE_POS = RAND;
|
||||
private static final float SPALTE_PRODUKT = 80;
|
||||
private static final float SPALTE_BEZEICHNUNG = 155;
|
||||
/** ... Zahlenspalten rechtsbündig an ihrer rechten Kante. */
|
||||
private static final float SPALTE_MENGE = 385;
|
||||
private static final float SPALTE_EINZELPREIS = 460;
|
||||
private static final float SPALTE_UST = 497;
|
||||
private static final float SPALTE_SUMME = RECHTS;
|
||||
|
||||
private final PDFont normal = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
private final PDFont fett = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD);
|
||||
|
|
@ -35,48 +64,11 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
|||
try (PDDocument pdf = new PDDocument()) {
|
||||
Schreiber schreiber = new Schreiber(pdf);
|
||||
|
||||
schreiber.zeile(fett, 16, dokument.belegtyp().anzeigename() + " " + dokument.getBelegnummer());
|
||||
if (dokument.getStatus() == DokumentStatus.STORNIERT) {
|
||||
schreiber.zeile(fett, 12, "*** STORNIERT ***");
|
||||
}
|
||||
schreiber.leer();
|
||||
schreiber.zeile(normal, 10, "Datum: " + format(dokument.getDatum()));
|
||||
if (dokument instanceof Angebot angebot) {
|
||||
schreiber.zeile(normal, 10, "Gültig bis: " + format(angebot.getGueltigBis()));
|
||||
}
|
||||
if (dokument instanceof Lieferschein lieferschein) {
|
||||
schreiber.zeile(normal, 10, "Lieferdatum: " + format(lieferschein.getLieferdatum()));
|
||||
}
|
||||
if (dokument instanceof Rechnung rechnung) {
|
||||
schreiber.zeile(normal, 10, "Leistungsdatum: " + format(rechnung.getLeistungsdatum()));
|
||||
schreiber.zeile(normal, 10, "Zahlbar bis: " + format(rechnung.getZahlungsziel()));
|
||||
}
|
||||
if (dokument.getVorgaengerNr() != null) {
|
||||
schreiber.zeile(normal, 10, "Referenz: " + dokument.getVorgaengerNr());
|
||||
}
|
||||
schreiber.leer();
|
||||
schreiber.zeile(fett, 10, "Kunde");
|
||||
schreiber.zeile(normal, 10, dokument.getKundeName() + " (" + dokument.getKundenReferenz() + ")");
|
||||
schreiber.zeile(normal, 10, dokument.getKundeAnschrift());
|
||||
schreiber.leer();
|
||||
|
||||
schreiber.zeile(fett, 10, String.format("%-4s %-10s %-34s %6s %12s %6s %12s",
|
||||
"Pos", "Produkt", "Bezeichnung", "Menge", "Einzelpreis", "USt", "Summe"));
|
||||
int pos = 1;
|
||||
for (Dokumentposition position : dokument.getPositionen()) {
|
||||
schreiber.zeile(normal, 10, String.format("%-4d %-10s %-34s %6d %12s %5s%% %12s",
|
||||
pos++,
|
||||
position.getProduktReferenz(),
|
||||
kuerze(position.getBezeichnung(), 34),
|
||||
position.getMenge(),
|
||||
betrag(position.getEinzelpreisNetto()),
|
||||
prozent(position.getSteuersatz()),
|
||||
betrag(position.getPositionssummeNetto())));
|
||||
}
|
||||
schreiber.leer();
|
||||
schreiber.zeile(normal, 10, "Summe netto: " + betrag(dokument.getSummeNetto()) + " EUR");
|
||||
schreiber.zeile(normal, 10, "Umsatzsteuer: " + betrag(dokument.getSummeSteuer()) + " EUR");
|
||||
schreiber.zeile(fett, 11, "Summe brutto: " + betrag(dokument.getSummeBrutto()) + " EUR");
|
||||
schreibeBriefkopf(schreiber, dokument);
|
||||
schreibeBelegkopf(schreiber, dokument);
|
||||
schreibePositionstabelle(schreiber, dokument);
|
||||
schreibeSummenblock(schreiber, dokument);
|
||||
schreibeSchlusstext(schreiber, dokument);
|
||||
|
||||
schreiber.schliesse();
|
||||
if (zielDatei.getParent() != null) {
|
||||
|
|
@ -88,15 +80,140 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
|||
}
|
||||
}
|
||||
|
||||
private static String format(java.time.LocalDate datum) {
|
||||
/** Absenderblock rechts oben, Rücksendezeile und Empfängerblock links. */
|
||||
private void schreibeBriefkopf(Schreiber schreiber, Dokument dokument) throws IOException {
|
||||
schreiber.rechtsbuendig(fett, 11, AUSSTELLER_NAME, RECHTS);
|
||||
schreiber.rechtsbuendig(normal, 9, AUSSTELLER_STRASSE, RECHTS);
|
||||
schreiber.rechtsbuendig(normal, 9, AUSSTELLER_ORT, RECHTS);
|
||||
schreiber.rechtsbuendig(normal, 9, AUSSTELLER_UST_ID, RECHTS);
|
||||
schreiber.leer();
|
||||
|
||||
schreiber.zeile(normal, 7, AUSSTELLER_NAME + " · " + AUSSTELLER_STRASSE
|
||||
+ " · " + AUSSTELLER_ORT);
|
||||
schreiber.linie();
|
||||
schreiber.zeile(normal, 10, dokument.getKundeName()
|
||||
+ " (Kundennr. " + dokument.getKundenReferenz() + ")");
|
||||
// anschrift() liefert "Straße, PLZ Ort" einzeilig; für den
|
||||
// Empfängerblock wird sie an der ersten Trennstelle umbrochen
|
||||
String anschrift = dokument.getKundeAnschrift();
|
||||
int trenner = anschrift == null ? -1 : anschrift.indexOf(", ");
|
||||
if (trenner >= 0) {
|
||||
schreiber.zeile(normal, 10, anschrift.substring(0, trenner));
|
||||
schreiber.zeile(normal, 10, anschrift.substring(trenner + 2));
|
||||
} else if (anschrift != null) {
|
||||
schreiber.zeile(normal, 10, anschrift);
|
||||
}
|
||||
schreiber.leer();
|
||||
schreiber.leer();
|
||||
}
|
||||
|
||||
/** Belegtitel, Stornokennzeichen und Datums-/Referenzangaben. */
|
||||
private void schreibeBelegkopf(Schreiber schreiber, Dokument dokument) throws IOException {
|
||||
schreiber.zeile(fett, 16, dokument.belegtyp().anzeigename() + " " + dokument.getBelegnummer());
|
||||
if (dokument.getStatus() == DokumentStatus.STORNIERT) {
|
||||
schreiber.zeile(fett, 12, "*** STORNIERT ***");
|
||||
}
|
||||
schreiber.leer();
|
||||
schreiber.zeile(normal, 10, "Datum: " + format(dokument.getDatum()));
|
||||
if (dokument instanceof Angebot angebot) {
|
||||
schreiber.zeile(normal, 10, "Gültig bis: " + format(angebot.getGueltigBis()));
|
||||
}
|
||||
if (dokument instanceof Lieferschein lieferschein) {
|
||||
schreiber.zeile(normal, 10, "Lieferdatum: " + format(lieferschein.getLieferdatum()));
|
||||
}
|
||||
if (dokument instanceof Rechnung rechnung) {
|
||||
schreiber.zeile(normal, 10, "Leistungsdatum: " + format(rechnung.getLeistungsdatum()));
|
||||
schreiber.zeile(normal, 10, "Zahlbar bis: " + format(rechnung.getZahlungsziel()));
|
||||
}
|
||||
if (dokument.getVorgaengerNr() != null) {
|
||||
schreiber.zeile(normal, 10, "Referenzbeleg: " + dokument.getVorgaengerNr());
|
||||
}
|
||||
schreiber.leer();
|
||||
}
|
||||
|
||||
/** Positionstabelle im festen Spaltenraster; Kopf wird je Seite wiederholt. */
|
||||
private void schreibePositionstabelle(Schreiber schreiber, Dokument dokument) throws IOException {
|
||||
schreibeTabellenkopf(schreiber);
|
||||
int pos = 1;
|
||||
for (Dokumentposition position : dokument.getPositionen()) {
|
||||
if (!schreiber.passtNochZeile()) {
|
||||
schreiber.neueSeite();
|
||||
schreibeTabellenkopf(schreiber);
|
||||
}
|
||||
schreiber.beginneZeile();
|
||||
schreiber.text(normal, 10, String.valueOf(pos++), SPALTE_POS);
|
||||
schreiber.text(normal, 10, position.getProduktReferenz(), SPALTE_PRODUKT);
|
||||
schreiber.text(normal, 10, kuerze(position.getBezeichnung(), 40), SPALTE_BEZEICHNUNG);
|
||||
schreiber.textRechts(normal, 10, String.valueOf(position.getMenge()), SPALTE_MENGE);
|
||||
schreiber.textRechts(normal, 10, betrag(position.getEinzelpreisNetto()), SPALTE_EINZELPREIS);
|
||||
schreiber.textRechts(normal, 10, prozent(position.getSteuersatz()) + " %", SPALTE_UST);
|
||||
schreiber.textRechts(normal, 10, betrag(position.getPositionssummeNetto()), SPALTE_SUMME);
|
||||
schreiber.beendeZeile();
|
||||
}
|
||||
schreiber.linie();
|
||||
}
|
||||
|
||||
private void schreibeTabellenkopf(Schreiber schreiber) throws IOException {
|
||||
schreiber.beginneZeile();
|
||||
schreiber.text(fett, 10, "Pos", SPALTE_POS);
|
||||
schreiber.text(fett, 10, "Produkt", SPALTE_PRODUKT);
|
||||
schreiber.text(fett, 10, "Bezeichnung", SPALTE_BEZEICHNUNG);
|
||||
schreiber.textRechts(fett, 10, "Menge", SPALTE_MENGE);
|
||||
schreiber.textRechts(fett, 10, "Einzelpreis", SPALTE_EINZELPREIS);
|
||||
schreiber.textRechts(fett, 10, "USt", SPALTE_UST);
|
||||
schreiber.textRechts(fett, 10, "Summe", SPALTE_SUMME);
|
||||
schreiber.beendeZeile();
|
||||
schreiber.linie();
|
||||
}
|
||||
|
||||
/** Summen rechtsbündig unter der Tabelle; Bruttosumme hervorgehoben (F-03). */
|
||||
private void schreibeSummenblock(Schreiber schreiber, Dokument dokument) throws IOException {
|
||||
summenzeile(schreiber, normal, 10, "Summe netto:", dokument.getSummeNetto());
|
||||
summenzeile(schreiber, normal, 10, "Umsatzsteuer:", dokument.getSummeSteuer());
|
||||
summenzeile(schreiber, fett, 11, "Summe brutto:", dokument.getSummeBrutto());
|
||||
schreiber.leer();
|
||||
}
|
||||
|
||||
private void summenzeile(Schreiber schreiber, PDFont font, float groesse,
|
||||
String beschriftung, BigDecimal wert) throws IOException {
|
||||
schreiber.beginneZeile();
|
||||
schreiber.textRechts(font, groesse, beschriftung, SPALTE_EINZELPREIS);
|
||||
schreiber.textRechts(font, groesse, betrag(wert) + " EUR", SPALTE_SUMME);
|
||||
schreiber.beendeZeile();
|
||||
}
|
||||
|
||||
/** Belegtyp-spezifischer Hinweistext am Ende des Dokuments. */
|
||||
private void schreibeSchlusstext(Schreiber schreiber, Dokument dokument) throws IOException {
|
||||
if (dokument instanceof Rechnung rechnung && rechnung.getZahlungsziel() != null
|
||||
&& dokument.getStatus() != DokumentStatus.STORNIERT) {
|
||||
schreiber.zeile(normal, 10, "Bitte überweisen Sie den Rechnungsbetrag bis zum "
|
||||
+ format(rechnung.getZahlungsziel()) + ".");
|
||||
}
|
||||
if (dokument instanceof Angebot angebot && angebot.getGueltigBis() != null) {
|
||||
schreiber.zeile(normal, 10, "Dieses Angebot ist gültig bis zum "
|
||||
+ format(angebot.getGueltigBis()) + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private static String format(LocalDate datum) {
|
||||
return datum == null ? "—" : DATUM.format(datum);
|
||||
}
|
||||
|
||||
/** Deutsches Betragsformat mit Tausenderpunkt, z. B. "1.234,56". */
|
||||
private static String betrag(BigDecimal wert) {
|
||||
return wert.toPlainString().replace('.', ',');
|
||||
if (wert == null) {
|
||||
return "—";
|
||||
}
|
||||
NumberFormat format = NumberFormat.getNumberInstance(Locale.GERMANY);
|
||||
format.setMinimumFractionDigits(2);
|
||||
format.setMaximumFractionDigits(2);
|
||||
return format.format(wert);
|
||||
}
|
||||
|
||||
private static String prozent(BigDecimal steuersatz) {
|
||||
if (steuersatz == null) {
|
||||
return "—";
|
||||
}
|
||||
return steuersatz.multiply(new BigDecimal("100")).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
|
|
@ -107,8 +224,13 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
|||
return text.length() <= maxLaenge ? text : text.substring(0, maxLaenge - 1) + "…";
|
||||
}
|
||||
|
||||
/** Zeilenweiser Schreiber mit automatischem Seitenumbruch. */
|
||||
private final class Schreiber {
|
||||
/**
|
||||
* Zeilenweiser Schreiber mit automatischem Seitenumbruch. Einfache
|
||||
* Zeilen entstehen über {@link #zeile}; Tabellenzeilen mit mehreren
|
||||
* Spalten über {@link #beginneZeile}, {@link #text}/{@link #textRechts}
|
||||
* und {@link #beendeZeile}.
|
||||
*/
|
||||
private static final class Schreiber {
|
||||
|
||||
private final PDDocument pdf;
|
||||
private PDPageContentStream inhalt;
|
||||
|
|
@ -119,7 +241,7 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
|||
neueSeite();
|
||||
}
|
||||
|
||||
private void neueSeite() throws IOException {
|
||||
void neueSeite() throws IOException {
|
||||
if (inhalt != null) {
|
||||
inhalt.close();
|
||||
}
|
||||
|
|
@ -129,16 +251,59 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
|||
y = PDRectangle.A4.getHeight() - RAND;
|
||||
}
|
||||
|
||||
void zeile(PDFont font, float groesse, String text) throws IOException {
|
||||
if (y < RAND + ZEILENHOEHE) {
|
||||
boolean passtNochZeile() {
|
||||
return y >= RAND + ZEILENHOEHE;
|
||||
}
|
||||
|
||||
/** Beginnt eine Tabellenzeile; bricht bei Bedarf auf eine neue Seite um. */
|
||||
void beginneZeile() throws IOException {
|
||||
if (!passtNochZeile()) {
|
||||
neueSeite();
|
||||
}
|
||||
}
|
||||
|
||||
void beendeZeile() {
|
||||
y -= ZEILENHOEHE;
|
||||
}
|
||||
|
||||
/** Linksbündiger Text an Spaltenposition {@code x} der aktuellen Zeile. */
|
||||
void text(PDFont font, float groesse, String text, float x) throws IOException {
|
||||
inhalt.beginText();
|
||||
inhalt.setFont(font, groesse);
|
||||
inhalt.newLineAtOffset(RAND, y);
|
||||
inhalt.showText(text);
|
||||
inhalt.newLineAtOffset(x, y);
|
||||
inhalt.showText(text == null ? "" : text);
|
||||
inhalt.endText();
|
||||
y -= ZEILENHOEHE;
|
||||
}
|
||||
|
||||
/** Rechtsbündiger Text: {@code xRechts} ist die rechte Kante der Spalte. */
|
||||
void textRechts(PDFont font, float groesse, String text, float xRechts) throws IOException {
|
||||
String sicher = text == null ? "" : text;
|
||||
float breite = font.getStringWidth(sicher) / 1000 * groesse;
|
||||
text(font, groesse, sicher, xRechts - breite);
|
||||
}
|
||||
|
||||
/** Einzelne linksbündige Zeile am linken Seitenrand. */
|
||||
void zeile(PDFont font, float groesse, String text) throws IOException {
|
||||
beginneZeile();
|
||||
text(font, groesse, text, RAND);
|
||||
beendeZeile();
|
||||
}
|
||||
|
||||
/** Einzelne rechtsbündige Zeile (z. B. Absenderblock). */
|
||||
void rechtsbuendig(PDFont font, float groesse, String text, float xRechts) throws IOException {
|
||||
beginneZeile();
|
||||
textRechts(font, groesse, text, xRechts);
|
||||
beendeZeile();
|
||||
}
|
||||
|
||||
/** Horizontale Trennlinie über die volle Satzspiegelbreite. */
|
||||
void linie() throws IOException {
|
||||
beginneZeile();
|
||||
inhalt.moveTo(RAND, y + ZEILENHOEHE - 4);
|
||||
inhalt.lineTo(RECHTS, y + ZEILENHOEHE - 4);
|
||||
inhalt.setLineWidth(0.5f);
|
||||
inhalt.stroke();
|
||||
y -= ZEILENHOEHE / 2;
|
||||
}
|
||||
|
||||
void leer() {
|
||||
|
|
|
|||
|
|
@ -10,13 +10,16 @@ import de.team1.faktura.kunden.KundenService;
|
|||
import de.team1.faktura.produkte.Produkt;
|
||||
import de.team1.faktura.produkte.ProduktService;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultComboBoxModel;
|
||||
import javax.swing.DefaultListModel;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSpinner;
|
||||
|
|
@ -74,10 +77,11 @@ public class BelegDialog extends JDialog {
|
|||
|
||||
private void baueOberflaeche() {
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
JPanel kopf = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(3, 3, 3, 3);
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
|
|
@ -98,7 +102,13 @@ public class BelegDialog extends JDialog {
|
|||
c.gridy = 2;
|
||||
kopf.add(datumBeschriftung, c);
|
||||
c.gridx = 1;
|
||||
datumFeld.setToolTipText("Optional — Format: TT.MM.JJJJ");
|
||||
kopf.add(datumFeld, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 3;
|
||||
c.gridwidth = 2;
|
||||
kopf.add(UiHilfen.pflichtfeldLegende(), c);
|
||||
add(kopf, BorderLayout.NORTH);
|
||||
|
||||
JPanel mitte = new JPanel(new BorderLayout(5, 5));
|
||||
|
|
@ -108,9 +118,11 @@ public class BelegDialog extends JDialog {
|
|||
eingabe.add(new JLabel("Menge:"));
|
||||
eingabe.add(mengeWahl);
|
||||
JButton hinzufuegen = new JButton("Hinzufügen");
|
||||
hinzufuegen.setMnemonic('H');
|
||||
hinzufuegen.addActionListener(e -> fuegePositionHinzu());
|
||||
eingabe.add(hinzufuegen);
|
||||
JButton entfernen = new JButton("Entfernen");
|
||||
entfernen.setMnemonic('N');
|
||||
entfernen.addActionListener(e -> entfernePosition());
|
||||
eingabe.add(entfernen);
|
||||
mitte.add(eingabe, BorderLayout.NORTH);
|
||||
|
|
@ -119,12 +131,32 @@ public class BelegDialog extends JDialog {
|
|||
|
||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton abbrechen = new JButton("Abbrechen");
|
||||
abbrechen.addActionListener(e -> dispose());
|
||||
abbrechen.setMnemonic('A');
|
||||
abbrechen.addActionListener(e -> abbrechenMitNachfrage());
|
||||
JButton erstellen = new JButton("Erstellen");
|
||||
erstellen.setMnemonic('E');
|
||||
erstellen.addActionListener(e -> erstelle());
|
||||
knoepfe.add(abbrechen);
|
||||
knoepfe.add(erstellen);
|
||||
add(knoepfe, BorderLayout.SOUTH);
|
||||
|
||||
getRootPane().setDefaultButton(erstellen);
|
||||
UiHilfen.escSchliesst(this, this::abbrechenMitNachfrage);
|
||||
UiHilfen.fensterSchliessenAbfangen(this, this::abbrechenMitNachfrage);
|
||||
}
|
||||
|
||||
/** Schutz vor Datenverlust: Nachfrage, wenn bereits Positionen erfasst wurden. */
|
||||
private void abbrechenMitNachfrage() {
|
||||
if (!positionen.isEmpty()) {
|
||||
int antwort = JOptionPane.showConfirmDialog(this,
|
||||
"Die erfassten Positionen gehen verloren. Dialog wirklich schließen?",
|
||||
"Eingaben verwerfen", JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispose();
|
||||
}
|
||||
|
||||
private void aktualisiereDatumsfeld() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.dokumente.Belegtyp;
|
||||
import de.team1.faktura.dokumente.Dokument;
|
||||
import de.team1.faktura.dokumente.DokumentCsvExport;
|
||||
import de.team1.faktura.dokumente.DokumentService;
|
||||
|
|
@ -9,6 +10,7 @@ import de.team1.faktura.gemeinsam.EreignisBus;
|
|||
import de.team1.faktura.kunden.KundenService;
|
||||
import de.team1.faktura.produkte.ProduktService;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
|
|
@ -27,6 +29,7 @@ import java.awt.Component;
|
|||
import java.awt.Desktop;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Window;
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
|
@ -61,9 +64,10 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
private final JButton folgebelegKnopf = new JButton("Folgebeleg erzeugen");
|
||||
private final JButton versendenKnopf = new JButton("Versenden");
|
||||
private final JButton stornierenKnopf = new JButton("Stornieren");
|
||||
private final JButton pdfKnopf = new JButton("PDF exportieren");
|
||||
private final JButton pdfKnopf = new JButton("PDF exportieren…");
|
||||
private final JButton druckenKnopf = new JButton("Drucken");
|
||||
private final JButton mailKnopf = new JButton("Per E-Mail senden");
|
||||
private final JLabel trefferAnzeige = UiHilfen.trefferLabel();
|
||||
|
||||
public DokumentListenPanel(DokumentService dokumentService,
|
||||
KundenService kundenService,
|
||||
|
|
@ -84,8 +88,9 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
|
||||
private void baueOberflaeche() {
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
JPanel kopf = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JPanel kopf = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
|
||||
kopf.add(new JLabel("Statusfilter:"));
|
||||
kopf.add(statusFilter);
|
||||
statusFilter.setRenderer(new DefaultListCellRenderer() {
|
||||
|
|
@ -99,10 +104,16 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
statusFilter.addActionListener(e -> aktualisiere());
|
||||
|
||||
JButton neueRechnung = new JButton("Neue Rechnung (Assistent)…");
|
||||
neueRechnung.setMnemonic('R');
|
||||
neueRechnung.setToolTipText("Geführte Rechnungserstellung in fünf Schritten (Alt+R)");
|
||||
neueRechnung.addActionListener(e -> oeffneWizard());
|
||||
JButton neuerBeleg = new JButton("Neuer Beleg…");
|
||||
neuerBeleg.setMnemonic('B');
|
||||
neuerBeleg.setToolTipText("Angebot, Auftragsbestätigung oder Lieferschein erstellen (Alt+B)");
|
||||
neuerBeleg.addActionListener(e -> oeffneBelegDialog());
|
||||
JButton datenExportKnopf = new JButton("Daten exportieren (CSV)…");
|
||||
JButton datenExportKnopf = new JButton("CSV exportieren…");
|
||||
datenExportKnopf.setMnemonic('C');
|
||||
datenExportKnopf.setToolTipText("Alle Belegdaten als CSV-Datei exportieren");
|
||||
datenExportKnopf.addActionListener(e -> exportiereDaten());
|
||||
kopf.add(neueRechnung);
|
||||
kopf.add(neuerBeleg);
|
||||
|
|
@ -114,9 +125,18 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
TabellenFormat.konfiguriere(tabelle);
|
||||
tabelle.getColumnModel().getColumn(4).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
||||
tabelle.getColumnModel().getColumn(5).setCellRenderer(TabellenFormat.statusRenderer());
|
||||
add(new JScrollPane(tabelle), BorderLayout.CENTER);
|
||||
JPanel listenSeite = new JPanel(new BorderLayout(0, 4));
|
||||
listenSeite.add(new JScrollPane(tabelle), BorderLayout.CENTER);
|
||||
listenSeite.add(trefferAnzeige, BorderLayout.SOUTH);
|
||||
add(listenSeite, BorderLayout.CENTER);
|
||||
|
||||
JPanel aktionen = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
folgebelegKnopf.setMnemonic('F');
|
||||
versendenKnopf.setMnemonic('V');
|
||||
stornierenKnopf.setMnemonic('O');
|
||||
pdfKnopf.setMnemonic('P');
|
||||
druckenKnopf.setMnemonic('D');
|
||||
mailKnopf.setMnemonic('E');
|
||||
folgebelegKnopf.addActionListener(e -> erzeugeFolgebeleg());
|
||||
versendenKnopf.addActionListener(e -> versende());
|
||||
stornierenKnopf.addActionListener(e -> storniere());
|
||||
|
|
@ -136,7 +156,7 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
private Dokument auswahl() {
|
||||
int zeile = tabelle.getSelectedRow();
|
||||
return zeile < 0 ? null
|
||||
: tabellenModel.dokumente.get(tabelle.convertRowIndexToModel(zeile));
|
||||
: tabellenModel.gibZeile(tabelle.convertRowIndexToModel(zeile));
|
||||
}
|
||||
|
||||
/** Aktiviert/deaktiviert die Belegaktionen gemäß Status (F-08, F-14). */
|
||||
|
|
@ -145,17 +165,22 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
if (dokument == null) {
|
||||
for (JButton knopf : List.of(folgebelegKnopf, versendenKnopf, stornierenKnopf,
|
||||
pdfKnopf, druckenKnopf, mailKnopf)) {
|
||||
knopf.setEnabled(false);
|
||||
UiHilfen.schalteAktion(knopf, false, "Bitte zuerst einen Beleg in der Liste auswählen.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
BelegAktionen verfuegbar = controller.aktionenFuer(dokument);
|
||||
folgebelegKnopf.setEnabled(dokument.belegtyp() != de.team1.faktura.dokumente.Belegtyp.RECHNUNG);
|
||||
versendenKnopf.setEnabled(verfuegbar.aenderbar());
|
||||
stornierenKnopf.setEnabled(verfuegbar.stornierbar());
|
||||
pdfKnopf.setEnabled(verfuegbar.pdfExport());
|
||||
druckenKnopf.setEnabled(verfuegbar.pdfExport());
|
||||
mailKnopf.setEnabled(verfuegbar.pdfExport());
|
||||
DokumentStatus status = dokument.getStatus();
|
||||
UiHilfen.schalteAktion(folgebelegKnopf, dokument.belegtyp() != Belegtyp.RECHNUNG,
|
||||
"Für Rechnungen wird kein Folgebeleg erzeugt.");
|
||||
UiHilfen.schalteAktion(versendenKnopf, verfuegbar.aenderbar(),
|
||||
"Im Status " + status + " sind keine inhaltlichen Änderungen mehr möglich (GR-02).");
|
||||
UiHilfen.schalteAktion(stornierenKnopf, verfuegbar.stornierbar(),
|
||||
"Stornieren ist nur für Rechnungen im Status OFFEN möglich (aktuell: " + status + ").");
|
||||
String keinPdf = "Für diesen Beleg ist kein PDF-Export möglich.";
|
||||
UiHilfen.schalteAktion(pdfKnopf, verfuegbar.pdfExport(), keinPdf);
|
||||
UiHilfen.schalteAktion(druckenKnopf, verfuegbar.pdfExport(), keinPdf);
|
||||
UiHilfen.schalteAktion(mailKnopf, verfuegbar.pdfExport(), keinPdf);
|
||||
}
|
||||
|
||||
private void oeffneWizard() {
|
||||
|
|
@ -226,23 +251,27 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
return;
|
||||
}
|
||||
JFileChooser auswahlDialog = new JFileChooser();
|
||||
auswahlDialog.setSelectedFile(new java.io.File(dokument.getBelegnummer() + ".pdf"));
|
||||
auswahlDialog.setSelectedFile(new File(dokument.getBelegnummer() + ".pdf"));
|
||||
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
Path ziel = auswahlDialog.getSelectedFile().toPath();
|
||||
dokumentService.exportierePdf(dokument.getBelegnummer(), ziel);
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das PDF wurde exportiert nach " + ziel), null);
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
dokumentService.exportierePdf(dokument.getBelegnummer(), ziel);
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das PDF wurde exportiert nach " + ziel), null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Vollständiger Datenexport aller Belege als CSV (Q-08, IF-04). */
|
||||
private void exportiereDaten() {
|
||||
JFileChooser auswahlDialog = new JFileChooser();
|
||||
auswahlDialog.setSelectedFile(new java.io.File("dokumente.csv"));
|
||||
auswahlDialog.setSelectedFile(new File("dokumente.csv"));
|
||||
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
Path ziel = auswahlDialog.getSelectedFile().toPath();
|
||||
datenExport.exportiereCsv(ziel);
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg(
|
||||
"Die Belegdaten wurden exportiert nach " + ziel), null);
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
datenExport.exportiereCsv(ziel);
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg(
|
||||
"Die Belegdaten wurden exportiert nach " + ziel), null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +318,17 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
|
||||
@Override
|
||||
public void aktualisiere() {
|
||||
tabellenModel.setze(controller.gefiltert((DokumentStatus) statusFilter.getSelectedItem()));
|
||||
DokumentStatus status = (DokumentStatus) statusFilter.getSelectedItem();
|
||||
List<Dokument> liste = controller.gefiltert(status);
|
||||
tabellenModel.setze(liste);
|
||||
int gesamt = status == null ? liste.size() : controller.gefiltert(null).size();
|
||||
if (gesamt == 0) {
|
||||
trefferAnzeige.setText("Noch keine Belege vorhanden");
|
||||
} else if (liste.isEmpty()) {
|
||||
trefferAnzeige.setText("Keine Belege im Status " + status);
|
||||
} else {
|
||||
trefferAnzeige.setText(liste.size() + " von " + gesamt + " Belegen");
|
||||
}
|
||||
aktualisiereAktionen();
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +352,10 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
|||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
Dokument gibZeile(int zeile) {
|
||||
return dokumente.get(zeile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return dokumente.size();
|
||||
|
|
|
|||
|
|
@ -1,25 +1,35 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.JToolBar;
|
||||
import javax.swing.KeyStroke;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.InputEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Hauptfenster mit Navigation zu den drei Modulen Kundenverwaltung,
|
||||
* Produktverwaltung und Dokumente (D-F-01). Beim Modulwechsel wird bei
|
||||
* ungespeicherten Formulareingaben nachgefragt (D-F-02).
|
||||
* Produktverwaltung und Dokumente (D-F-01). Das aktive Modul ist in der
|
||||
* Navigationsleiste sichtbar markiert; Alt+1/2/3 wechseln direkt. Beim
|
||||
* Modulwechsel wird bei ungespeicherten Formulareingaben nachgefragt (D-F-02).
|
||||
*/
|
||||
public class HauptFenster extends JFrame {
|
||||
|
||||
private final CardLayout karten = new CardLayout();
|
||||
private final JPanel kartenPanel = new JPanel(karten);
|
||||
private final Map<String, ModulPanel> module = new LinkedHashMap<>();
|
||||
private final Map<String, JToggleButton> navigationsKnoepfe = new LinkedHashMap<>();
|
||||
|
||||
private String aktuellesModul;
|
||||
|
||||
|
|
@ -35,10 +45,18 @@ public class HauptFenster extends JFrame {
|
|||
|
||||
JToolBar navigation = new JToolBar();
|
||||
navigation.setFloatable(false);
|
||||
ButtonGroup gruppe = new ButtonGroup();
|
||||
int modulNummer = 1;
|
||||
for (Map.Entry<String, ModulPanel> eintrag : module.entrySet()) {
|
||||
JButton knopf = new JButton(eintrag.getKey());
|
||||
JToggleButton knopf = new JToggleButton(eintrag.getKey());
|
||||
knopf.setMargin(new Insets(6, 14, 6, 14));
|
||||
knopf.setToolTipText(eintrag.getKey() + " anzeigen (Alt+" + modulNummer + ")");
|
||||
knopf.addActionListener(e -> wechsleZu(eintrag.getKey()));
|
||||
gruppe.add(knopf);
|
||||
navigation.add(knopf);
|
||||
navigationsKnoepfe.put(eintrag.getKey(), knopf);
|
||||
bindeModulTaste(eintrag.getKey(), modulNummer);
|
||||
modulNummer++;
|
||||
}
|
||||
add(navigation, BorderLayout.NORTH);
|
||||
|
||||
|
|
@ -48,15 +66,29 @@ public class HauptFenster extends JFrame {
|
|||
add(kartenPanel, BorderLayout.CENTER);
|
||||
|
||||
aktuellesModul = "Kunden";
|
||||
navigationsKnoepfe.get(aktuellesModul).setSelected(true);
|
||||
karten.show(kartenPanel, aktuellesModul);
|
||||
|
||||
setSize(1100, 650);
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
|
||||
/** Alt+1/2/3 wechselt direkt zum jeweiligen Modul. */
|
||||
private void bindeModulTaste(String modulName, int nummer) {
|
||||
KeyStroke taste = KeyStroke.getKeyStroke(KeyEvent.VK_0 + nummer, InputEvent.ALT_DOWN_MASK);
|
||||
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(taste, "modul" + nummer);
|
||||
getRootPane().getActionMap().put("modul" + nummer, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
wechsleZu(modulName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Modulwechsel mit Nachfrage bei ungespeicherten Eingaben (D-F-02). */
|
||||
private void wechsleZu(String modulName) {
|
||||
if (modulName.equals(aktuellesModul)) {
|
||||
navigationsKnoepfe.get(aktuellesModul).setSelected(true);
|
||||
return;
|
||||
}
|
||||
ModulPanel aktuell = module.get(aktuellesModul);
|
||||
|
|
@ -66,10 +98,13 @@ public class HauptFenster extends JFrame {
|
|||
"Ungespeicherte Eingaben", JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
// Wechsel abgebrochen: Markierung zurück auf das aktuelle Modul
|
||||
navigationsKnoepfe.get(aktuellesModul).setSelected(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
aktuellesModul = modulName;
|
||||
navigationsKnoepfe.get(modulName).setSelected(true);
|
||||
module.get(modulName).aktualisiere();
|
||||
karten.show(kartenPanel, modulName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.kunden.Kunde;
|
||||
import de.team1.faktura.kunden.KundenVerwaltungsService;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Window;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Modale Formular-Maske zum Anlegen und Bearbeiten eines Kunden (D-F-04,
|
||||
* F-05): alle Pflicht- und optionalen Felder mit Kennzeichnung, Validierungs-
|
||||
* rückmeldung mit Feldmarkierung und Schutz vor unbeabsichtigtem Verwerfen.
|
||||
*/
|
||||
public class KundenFormularDialog extends JDialog {
|
||||
|
||||
private final KundenVerwaltungsService service;
|
||||
/** Kundennummer des bearbeiteten Kunden; {@code null} = Neuanlage. */
|
||||
private final String vorhandeneNummer;
|
||||
|
||||
private final JTextField nameFeld = new JTextField(20);
|
||||
private final JTextField strasseFeld = new JTextField(20);
|
||||
private final JTextField plzFeld = new JTextField(8);
|
||||
private final JTextField ortFeld = new JTextField(20);
|
||||
private final JTextField eMailFeld = new JTextField(20);
|
||||
private final JTextField telefonFeld = new JTextField(20);
|
||||
private final JTextField ustIdNrFeld = new JTextField(20);
|
||||
private final Map<String, JComponent> felder = new LinkedHashMap<>();
|
||||
|
||||
private boolean ungespeichert;
|
||||
|
||||
public KundenFormularDialog(Window besitzer, KundenVerwaltungsService service,
|
||||
Kunde vorhandener) {
|
||||
super(besitzer, vorhandener == null ? "Neuen Kunden anlegen"
|
||||
: "Kunde " + vorhandener.getKundennummer() + " bearbeiten",
|
||||
ModalityType.APPLICATION_MODAL);
|
||||
this.service = service;
|
||||
this.vorhandeneNummer = vorhandener == null ? null : vorhandener.getKundennummer();
|
||||
felder.put("Name", nameFeld);
|
||||
felder.put("Straße", strasseFeld);
|
||||
felder.put("PLZ", plzFeld);
|
||||
felder.put("Ort", ortFeld);
|
||||
felder.put("E-Mail", eMailFeld);
|
||||
baueOberflaeche();
|
||||
if (vorhandener != null) {
|
||||
fuelleFelder(vorhandener);
|
||||
}
|
||||
beobachteAenderungen();
|
||||
pack();
|
||||
setLocationRelativeTo(besitzer);
|
||||
}
|
||||
|
||||
private void baueOberflaeche() {
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
JPanel formular = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
plzFeld.setToolTipText("Pflichtfeld — Postleitzahl, z. B. 68163");
|
||||
eMailFeld.setToolTipText("Optional — Format: name@domain.de");
|
||||
|
||||
int zeile = 0;
|
||||
zeile = formularZeile(formular, c, zeile, "Name: *", nameFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Straße: *", strasseFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "PLZ: *", plzFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Ort: *", ortFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "E-Mail:", eMailFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Telefon:", telefonFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "USt-IdNr.:", ustIdNrFeld);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 2;
|
||||
formular.add(UiHilfen.pflichtfeldLegende(), c);
|
||||
add(formular, BorderLayout.CENTER);
|
||||
|
||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton abbrechen = new JButton("Abbrechen");
|
||||
abbrechen.setMnemonic('A');
|
||||
abbrechen.addActionListener(e -> abbrechenMitNachfrage());
|
||||
JButton speichern = new JButton("Speichern");
|
||||
speichern.setMnemonic('S');
|
||||
speichern.addActionListener(e -> speichere());
|
||||
knoepfe.add(abbrechen);
|
||||
knoepfe.add(speichern);
|
||||
add(knoepfe, BorderLayout.SOUTH);
|
||||
|
||||
getRootPane().setDefaultButton(speichern);
|
||||
UiHilfen.escSchliesst(this, this::abbrechenMitNachfrage);
|
||||
UiHilfen.fensterSchliessenAbfangen(this, this::abbrechenMitNachfrage);
|
||||
}
|
||||
|
||||
private int formularZeile(JPanel formular, GridBagConstraints c, int zeile,
|
||||
String beschriftung, JComponent feld) {
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 1;
|
||||
c.weightx = 0;
|
||||
formular.add(new JLabel(beschriftung), c);
|
||||
c.gridx = 1;
|
||||
c.weightx = 1;
|
||||
formular.add(feld, c);
|
||||
return zeile + 1;
|
||||
}
|
||||
|
||||
private void fuelleFelder(Kunde kunde) {
|
||||
nameFeld.setText(kunde.getName());
|
||||
strasseFeld.setText(kunde.getStrasse());
|
||||
plzFeld.setText(kunde.getPlz());
|
||||
ortFeld.setText(kunde.getOrt());
|
||||
eMailFeld.setText(kunde.getEMail() == null ? "" : kunde.getEMail());
|
||||
telefonFeld.setText(kunde.getTelefon() == null ? "" : kunde.getTelefon());
|
||||
ustIdNrFeld.setText(kunde.getUstIdNr() == null ? "" : kunde.getUstIdNr());
|
||||
}
|
||||
|
||||
/** Erst nach dem Vorbefüllen anmelden, damit nur Nutzereingaben zählen. */
|
||||
private void beobachteAenderungen() {
|
||||
DocumentListener listener = new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
};
|
||||
for (JTextField feld : List.of(nameFeld, strasseFeld, plzFeld, ortFeld,
|
||||
eMailFeld, telefonFeld, ustIdNrFeld)) {
|
||||
feld.getDocument().addDocumentListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void speichere() {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
Kunde kunde = vorhandeneNummer == null
|
||||
? new Kunde()
|
||||
: service.findeKunde(vorhandeneNummer);
|
||||
kunde.setName(nameFeld.getText().trim());
|
||||
kunde.setStrasse(strasseFeld.getText().trim());
|
||||
kunde.setPlz(plzFeld.getText().trim());
|
||||
kunde.setOrt(ortFeld.getText().trim());
|
||||
kunde.setEMail(leerZuNull(eMailFeld.getText()));
|
||||
kunde.setTelefon(leerZuNull(telefonFeld.getText()));
|
||||
kunde.setUstIdNr(leerZuNull(ustIdNrFeld.getText()));
|
||||
|
||||
Kunde gespeichert = vorhandeneNummer == null
|
||||
? service.legeAn(kunde)
|
||||
: service.aendere(kunde);
|
||||
ungespeichert = false;
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gespeichert. Kundennummer: "
|
||||
+ gespeichert.getKundennummer()), felder);
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/** Schutz vor Datenverlust: Nachfrage, wenn bereits Eingaben geändert wurden. */
|
||||
private void abbrechenMitNachfrage() {
|
||||
if (ungespeichert) {
|
||||
int antwort = JOptionPane.showConfirmDialog(this,
|
||||
"Die Eingaben gehen verloren. Maske wirklich schließen?",
|
||||
"Eingaben verwerfen", JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispose();
|
||||
}
|
||||
|
||||
private static String leerZuNull(String text) {
|
||||
String wert = text.trim();
|
||||
return wert.isEmpty() ? null : wert;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,65 +6,59 @@ import de.team1.faktura.kunden.Kunde;
|
|||
import de.team1.faktura.kunden.KundenCsvExport;
|
||||
import de.team1.faktura.kunden.KundenVerwaltungsService;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Modulansicht Kundenverwaltung (D-F-03 bis F-05): sortierte Liste mit
|
||||
* Suchfeld, Formular mit gekennzeichneten Pflichtfeldern, CSV-Export.
|
||||
* Modulansicht Kundenverwaltung (D-F-03 bis F-05): sortierte Liste in voller
|
||||
* Breite mit Suchfeld; Anlegen und Bearbeiten erfolgen über die modale
|
||||
* Formular-Maske {@link KundenFormularDialog} (Aufruf per Knopf oder
|
||||
* Doppelklick), die Aktionen liegen unter der Tabelle.
|
||||
*/
|
||||
public class KundenPanel extends JPanel implements ModulPanel {
|
||||
|
||||
private final KundenVerwaltungsService service;
|
||||
private final StammdatenController controller;
|
||||
private final KundenCsvExport csvExport;
|
||||
|
||||
private final JTextField suchfeld = new JTextField(20);
|
||||
private final KundenTabellenModel tabellenModel = new KundenTabellenModel();
|
||||
private final JTable tabelle = new JTable(tabellenModel);
|
||||
private final JLabel trefferAnzeige = UiHilfen.trefferLabel();
|
||||
|
||||
private final JTextField nameFeld = new JTextField(20);
|
||||
private final JTextField strasseFeld = new JTextField(20);
|
||||
private final JTextField plzFeld = new JTextField(8);
|
||||
private final JTextField ortFeld = new JTextField(20);
|
||||
private final JTextField eMailFeld = new JTextField(20);
|
||||
private final JTextField telefonFeld = new JTextField(20);
|
||||
private final JTextField ustIdNrFeld = new JTextField(20);
|
||||
private final JLabel nummerAnzeige = new JLabel("— neuer Kunde —");
|
||||
private final Map<String, JComponent> felder = new LinkedHashMap<>();
|
||||
private final JButton bearbeitenKnopf = new JButton("Bearbeiten…");
|
||||
private final JButton loeschenKnopf = new JButton("Löschen");
|
||||
|
||||
private String gewaehlteNummer;
|
||||
private boolean ungespeichert;
|
||||
|
||||
public KundenPanel(KundenVerwaltungsService service, KundenCsvExport csvExport,
|
||||
EreignisBus ereignisBus) {
|
||||
/**
|
||||
* @param service Fachkomponente der Gruppe C für Anlegen/Ändern/Löschen
|
||||
* @param controller GUI-freier Controller für Suche und Listeninhalt (D-F-03)
|
||||
* @param csvExport CSV-Export der Kundenstammdaten (C-F-13)
|
||||
* @param ereignisBus Observer-Verteiler für Aktualisierungen nach Datenänderungen
|
||||
*/
|
||||
public KundenPanel(KundenVerwaltungsService service, StammdatenController controller,
|
||||
KundenCsvExport csvExport, EreignisBus ereignisBus) {
|
||||
this.service = service;
|
||||
this.controller = controller;
|
||||
this.csvExport = csvExport;
|
||||
felder.put("Name", nameFeld);
|
||||
felder.put("Straße", strasseFeld);
|
||||
felder.put("PLZ", plzFeld);
|
||||
felder.put("Ort", ortFeld);
|
||||
felder.put("E-Mail", eMailFeld);
|
||||
baueOberflaeche();
|
||||
aktualisiere();
|
||||
ereignisBus.abonniere(DatenBereich.KUNDEN, this::aktualisiere);
|
||||
|
|
@ -72,180 +66,124 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
|
||||
private void baueOberflaeche() {
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
suchleiste.add(new JLabel("Suche (Name oder Kundennummer):"));
|
||||
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
|
||||
suchleiste.add(new JLabel("Suche:"));
|
||||
UiHilfen.platzhalter(suchfeld, "Name oder Kundennummer…");
|
||||
suchfeld.setToolTipText("Suche nach Name oder Kundennummer (Strg+F)");
|
||||
suchleiste.add(suchfeld);
|
||||
suchfeld.getDocument().addDocumentListener(neuerSuchListener());
|
||||
add(suchleiste, BorderLayout.NORTH);
|
||||
|
||||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
TabellenFormat.konfiguriere(tabelle);
|
||||
tabelle.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) {
|
||||
ladeAuswahl();
|
||||
tabelle.getSelectionModel().addListSelectionListener(e -> aktualisiereAktionen());
|
||||
tabelle.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
bearbeite();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
JSplitPane teiler = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
|
||||
new JScrollPane(tabelle), baueFormular());
|
||||
teiler.setResizeWeight(0.55);
|
||||
add(teiler, BorderLayout.CENTER);
|
||||
JPanel listenSeite = new JPanel(new BorderLayout(0, 4));
|
||||
listenSeite.add(new JScrollPane(tabelle), BorderLayout.CENTER);
|
||||
listenSeite.add(trefferAnzeige, BorderLayout.SOUTH);
|
||||
add(listenSeite, BorderLayout.CENTER);
|
||||
|
||||
JPanel aktionen = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JButton neuKnopf = new JButton("Neuen Kunden anlegen…");
|
||||
neuKnopf.setMnemonic('N');
|
||||
neuKnopf.setToolTipText("Öffnet die Maske für einen neuen Kunden (Alt+N)");
|
||||
neuKnopf.addActionListener(e -> legeNeuAn());
|
||||
bearbeitenKnopf.setMnemonic('B');
|
||||
bearbeitenKnopf.addActionListener(e -> bearbeite());
|
||||
loeschenKnopf.setMnemonic('L');
|
||||
loeschenKnopf.addActionListener(e -> loesche());
|
||||
JButton exportKnopf = new JButton("CSV exportieren…");
|
||||
exportKnopf.setMnemonic('C');
|
||||
exportKnopf.addActionListener(e -> exportiere());
|
||||
aktionen.add(neuKnopf);
|
||||
aktionen.add(bearbeitenKnopf);
|
||||
aktionen.add(loeschenKnopf);
|
||||
aktionen.add(exportKnopf);
|
||||
add(aktionen, BorderLayout.SOUTH);
|
||||
|
||||
UiHilfen.strgF(this, suchfeld);
|
||||
aktualisiereAktionen();
|
||||
}
|
||||
|
||||
private JPanel baueFormular() {
|
||||
JPanel formular = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(3, 3, 3, 3);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
private Kunde auswahl() {
|
||||
int zeile = tabelle.getSelectedRow();
|
||||
return zeile < 0 ? null
|
||||
: tabellenModel.gibZeile(tabelle.convertRowIndexToModel(zeile));
|
||||
}
|
||||
|
||||
int zeile = 0;
|
||||
zeile = formularZeile(formular, c, zeile, "Kundennummer:", nummerAnzeige);
|
||||
zeile = formularZeile(formular, c, zeile, "Name: *", nameFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Straße: *", strasseFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "PLZ: *", plzFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Ort: *", ortFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "E-Mail:", eMailFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Telefon:", telefonFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "USt-IdNr.:", ustIdNrFeld);
|
||||
private void aktualisiereAktionen() {
|
||||
boolean ausgewaehlt = auswahl() != null;
|
||||
String hinweis = "Bitte zuerst einen Kunden in der Liste auswählen.";
|
||||
UiHilfen.schalteAktion(bearbeitenKnopf, ausgewaehlt, hinweis);
|
||||
UiHilfen.schalteAktion(loeschenKnopf, ausgewaehlt, hinweis);
|
||||
}
|
||||
|
||||
DocumentListener aenderungsListener = neuerAenderungsListener();
|
||||
for (JComponent feld : List.of(nameFeld, strasseFeld, plzFeld, ortFeld,
|
||||
eMailFeld, telefonFeld, ustIdNrFeld)) {
|
||||
((JTextField) feld).getDocument().addDocumentListener(aenderungsListener);
|
||||
private void legeNeuAn() {
|
||||
new KundenFormularDialog(SwingUtilities.getWindowAncestor(this), service, null)
|
||||
.setVisible(true);
|
||||
}
|
||||
|
||||
private void bearbeite() {
|
||||
Kunde kunde = auswahl();
|
||||
if (kunde == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JButton neu = new JButton("Neu");
|
||||
neu.addActionListener(e -> leereFormular());
|
||||
JButton speichern = new JButton("Speichern");
|
||||
speichern.addActionListener(e -> speichere());
|
||||
JButton loeschen = new JButton("Löschen");
|
||||
loeschen.addActionListener(e -> loesche());
|
||||
JButton export = new JButton("CSV-Export");
|
||||
export.addActionListener(e -> exportiere());
|
||||
knoepfe.add(neu);
|
||||
knoepfe.add(speichern);
|
||||
knoepfe.add(loeschen);
|
||||
knoepfe.add(export);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 2;
|
||||
formular.add(knoepfe, c);
|
||||
return formular;
|
||||
}
|
||||
|
||||
private int formularZeile(JPanel formular, GridBagConstraints c, int zeile,
|
||||
String beschriftung, JComponent feld) {
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 1;
|
||||
c.weightx = 0;
|
||||
formular.add(new JLabel(beschriftung), c);
|
||||
c.gridx = 1;
|
||||
c.weightx = 1;
|
||||
formular.add(feld, c);
|
||||
return zeile + 1;
|
||||
}
|
||||
|
||||
private void speichere() {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
Kunde kunde = gewaehlteNummer == null
|
||||
? new Kunde()
|
||||
: service.findeKunde(gewaehlteNummer);
|
||||
kunde.setName(nameFeld.getText().trim());
|
||||
kunde.setStrasse(strasseFeld.getText().trim());
|
||||
kunde.setPlz(plzFeld.getText().trim());
|
||||
kunde.setOrt(ortFeld.getText().trim());
|
||||
kunde.setEMail(leerZuNull(eMailFeld.getText()));
|
||||
kunde.setTelefon(leerZuNull(telefonFeld.getText()));
|
||||
kunde.setUstIdNr(leerZuNull(ustIdNrFeld.getText()));
|
||||
|
||||
Kunde gespeichert = gewaehlteNummer == null
|
||||
? service.legeAn(kunde)
|
||||
: service.aendere(kunde);
|
||||
ungespeichert = false;
|
||||
gewaehlteNummer = gespeichert.getKundennummer();
|
||||
nummerAnzeige.setText(gespeichert.getKundennummer());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gespeichert. Kundennummer: "
|
||||
+ gespeichert.getKundennummer()), felder);
|
||||
});
|
||||
new KundenFormularDialog(SwingUtilities.getWindowAncestor(this), service, kunde)
|
||||
.setVisible(true);
|
||||
}
|
||||
|
||||
private void loesche() {
|
||||
if (gewaehlteNummer == null) {
|
||||
Kunde kunde = auswahl();
|
||||
if (kunde == null) {
|
||||
return;
|
||||
}
|
||||
int antwort = JOptionPane.showConfirmDialog(this,
|
||||
"Kunde " + gewaehlteNummer + " wirklich dauerhaft löschen?",
|
||||
"Kunde " + kunde.getKundennummer() + " wirklich dauerhaft löschen?",
|
||||
"Kunde löschen", JOptionPane.YES_NO_OPTION);
|
||||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
service.loescheKunde(gewaehlteNummer);
|
||||
leereFormular();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gelöscht."), felder);
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
service.loescheKunde(kunde.getKundennummer());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gelöscht."), null);
|
||||
});
|
||||
}
|
||||
|
||||
private void exportiere() {
|
||||
JFileChooser auswahl = new JFileChooser();
|
||||
auswahl.setSelectedFile(new java.io.File("kunden.csv"));
|
||||
if (auswahl.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
csvExport.exportiereCsv(auswahl.getSelectedFile().toPath());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Die Kundenstammdaten wurden exportiert nach "
|
||||
+ auswahl.getSelectedFile()), felder);
|
||||
JFileChooser auswahlDialog = new JFileChooser();
|
||||
auswahlDialog.setSelectedFile(new File("kunden.csv"));
|
||||
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
csvExport.exportiereCsv(auswahlDialog.getSelectedFile().toPath());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Die Kundenstammdaten wurden exportiert nach "
|
||||
+ auswahlDialog.getSelectedFile()), null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ladeAuswahl() {
|
||||
int zeile = tabelle.getSelectedRow();
|
||||
if (zeile < 0) {
|
||||
return;
|
||||
}
|
||||
Kunde kunde = tabellenModel.kunden.get(tabelle.convertRowIndexToModel(zeile));
|
||||
gewaehlteNummer = kunde.getKundennummer();
|
||||
nummerAnzeige.setText(kunde.getKundennummer());
|
||||
nameFeld.setText(kunde.getName());
|
||||
strasseFeld.setText(kunde.getStrasse());
|
||||
plzFeld.setText(kunde.getPlz());
|
||||
ortFeld.setText(kunde.getOrt());
|
||||
eMailFeld.setText(kunde.getEMail() == null ? "" : kunde.getEMail());
|
||||
telefonFeld.setText(kunde.getTelefon() == null ? "" : kunde.getTelefon());
|
||||
ustIdNrFeld.setText(kunde.getUstIdNr() == null ? "" : kunde.getUstIdNr());
|
||||
ungespeichert = false;
|
||||
}
|
||||
|
||||
private void leereFormular() {
|
||||
gewaehlteNummer = null;
|
||||
nummerAnzeige.setText("— neuer Kunde —");
|
||||
for (JComponent feld : List.of(nameFeld, strasseFeld, plzFeld, ortFeld,
|
||||
eMailFeld, telefonFeld, ustIdNrFeld)) {
|
||||
((JTextField) feld).setText("");
|
||||
}
|
||||
tabelle.clearSelection();
|
||||
ungespeichert = false;
|
||||
}
|
||||
|
||||
private static String leerZuNull(String text) {
|
||||
String wert = text.trim();
|
||||
return wert.isEmpty() ? null : wert;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hatUngespeicherteAenderungen() {
|
||||
return ungespeichert;
|
||||
// Eingaben erfolgen ausschließlich in der modalen Formular-Maske;
|
||||
// dort schützt die eigene Nachfrage vor Datenverlust (D-F-02).
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void aktualisiere() {
|
||||
String begriff = suchfeld.getText().trim();
|
||||
tabellenModel.setze(begriff.isEmpty()
|
||||
? service.alleSortiertNachName()
|
||||
: service.suche(begriff));
|
||||
List<Kunde> liste = controller.kundenListe(suchfeld.getText());
|
||||
tabellenModel.setze(liste);
|
||||
trefferAnzeige.setText(UiHilfen.trefferText(liste.size(),
|
||||
controller.kundenListe("").size(), "Kunden", suchfeld.getText()));
|
||||
}
|
||||
|
||||
private DocumentListener neuerSuchListener() {
|
||||
|
|
@ -267,25 +205,6 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
};
|
||||
}
|
||||
|
||||
private DocumentListener neuerAenderungsListener() {
|
||||
return new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static final class KundenTabellenModel extends AbstractTableModel {
|
||||
|
||||
private static final String[] SPALTEN = {"Kundennummer", "Name", "Straße", "PLZ", "Ort"};
|
||||
|
|
@ -297,6 +216,10 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
|||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
Kunde gibZeile(int zeile) {
|
||||
return kunden.get(zeile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return kunden.size();
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ package de.team1.faktura.gui;
|
|||
*/
|
||||
public record Meldung(MeldungsTyp typ, String feldname, String text) {
|
||||
|
||||
/** Erfolgsmeldung ohne Feldbezug (D-F-17). */
|
||||
public static Meldung erfolg(String text) {
|
||||
return new Meldung(MeldungsTyp.ERFOLG, null, text);
|
||||
}
|
||||
|
||||
/** Fehlermeldung; {@code feldname} benennt das betroffene Eingabefeld (Q-09). */
|
||||
public static Meldung fehler(String feldname, String text) {
|
||||
return new Meldung(MeldungsTyp.FEHLER, feldname, text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ public final class MeldungsAnzeige {
|
|||
} catch (UncheckedIOException e) {
|
||||
zeige(parent, Meldung.fehler(null,
|
||||
"Die Daten konnten nicht gespeichert werden: " + e.getMessage()), felder);
|
||||
} catch (RuntimeException e) {
|
||||
// Letztes Netz: kein stilles Scheitern auf dem Event-Dispatch-Thread
|
||||
zeige(parent, Meldung.fehler(null, "Unerwarteter Fehler: " + e), felder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
||||
import de.team1.faktura.produkte.Produkt;
|
||||
import de.team1.faktura.produkte.ProduktVerwaltungsService;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.Window;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Modale Formular-Maske zum Anlegen und Bearbeiten eines Produkts (D-F-04,
|
||||
* F-05): alle Pflicht- und optionalen Felder mit Kennzeichnung, Validierungs-
|
||||
* rückmeldung mit Feldmarkierung und Schutz vor unbeabsichtigtem Verwerfen.
|
||||
*/
|
||||
public class ProduktFormularDialog extends JDialog {
|
||||
|
||||
private static final String[] STEUERSAETZE = {"19 %", "7 %", "0 %"};
|
||||
|
||||
private final ProduktVerwaltungsService service;
|
||||
/** Produktnummer des bearbeiteten Produkts; {@code null} = Neuanlage. */
|
||||
private final String vorhandeneNummer;
|
||||
|
||||
private final JTextField bezeichnungFeld = new JTextField(20);
|
||||
private final JTextField beschreibungFeld = new JTextField(20);
|
||||
private final JTextField preisFeld = new JTextField(10);
|
||||
private final JComboBox<String> steuersatzWahl = new JComboBox<>(STEUERSAETZE);
|
||||
private final JTextField einheitFeld = new JTextField(10);
|
||||
private final Map<String, JComponent> felder = new LinkedHashMap<>();
|
||||
|
||||
private boolean ungespeichert;
|
||||
|
||||
public ProduktFormularDialog(Window besitzer, ProduktVerwaltungsService service,
|
||||
Produkt vorhandenes) {
|
||||
super(besitzer, vorhandenes == null ? "Neues Produkt anlegen"
|
||||
: "Produkt " + vorhandenes.getProduktnummer() + " bearbeiten",
|
||||
ModalityType.APPLICATION_MODAL);
|
||||
this.service = service;
|
||||
this.vorhandeneNummer = vorhandenes == null ? null : vorhandenes.getProduktnummer();
|
||||
felder.put("Bezeichnung", bezeichnungFeld);
|
||||
felder.put("Einzelpreis", preisFeld);
|
||||
felder.put("Steuersatz", steuersatzWahl);
|
||||
baueOberflaeche();
|
||||
if (vorhandenes != null) {
|
||||
fuelleFelder(vorhandenes);
|
||||
}
|
||||
beobachteAenderungen();
|
||||
pack();
|
||||
setLocationRelativeTo(besitzer);
|
||||
}
|
||||
|
||||
private void baueOberflaeche() {
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
JPanel formular = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(4, 4, 4, 4);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
preisFeld.setToolTipText("Pflichtfeld — z. B. 19,90 (Komma oder Punkt als Dezimaltrennzeichen)");
|
||||
|
||||
int zeile = 0;
|
||||
zeile = formularZeile(formular, c, zeile, "Bezeichnung: *", bezeichnungFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Beschreibung:", beschreibungFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Einzelpreis (netto): *", preisFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Steuersatz: *", steuersatzWahl);
|
||||
zeile = formularZeile(formular, c, zeile, "Einheit:", einheitFeld);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 2;
|
||||
formular.add(UiHilfen.pflichtfeldLegende(), c);
|
||||
add(formular, BorderLayout.CENTER);
|
||||
|
||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton abbrechen = new JButton("Abbrechen");
|
||||
abbrechen.setMnemonic('A');
|
||||
abbrechen.addActionListener(e -> abbrechenMitNachfrage());
|
||||
JButton speichern = new JButton("Speichern");
|
||||
speichern.setMnemonic('S');
|
||||
speichern.addActionListener(e -> speichere());
|
||||
knoepfe.add(abbrechen);
|
||||
knoepfe.add(speichern);
|
||||
add(knoepfe, BorderLayout.SOUTH);
|
||||
|
||||
getRootPane().setDefaultButton(speichern);
|
||||
UiHilfen.escSchliesst(this, this::abbrechenMitNachfrage);
|
||||
UiHilfen.fensterSchliessenAbfangen(this, this::abbrechenMitNachfrage);
|
||||
}
|
||||
|
||||
private int formularZeile(JPanel formular, GridBagConstraints c, int zeile,
|
||||
String beschriftung, JComponent feld) {
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 1;
|
||||
c.weightx = 0;
|
||||
formular.add(new JLabel(beschriftung), c);
|
||||
c.gridx = 1;
|
||||
c.weightx = 1;
|
||||
formular.add(feld, c);
|
||||
return zeile + 1;
|
||||
}
|
||||
|
||||
private void fuelleFelder(Produkt produkt) {
|
||||
bezeichnungFeld.setText(produkt.getBezeichnung());
|
||||
beschreibungFeld.setText(produkt.getBeschreibung() == null ? "" : produkt.getBeschreibung());
|
||||
preisFeld.setText(produkt.getEinzelpreisNetto().toPlainString());
|
||||
steuersatzWahl.setSelectedIndex(switch (produkt.getSteuersatz().stripTrailingZeros().toPlainString()) {
|
||||
case "0.19" -> 0;
|
||||
case "0.07" -> 1;
|
||||
default -> 2;
|
||||
});
|
||||
einheitFeld.setText(produkt.getEinheit() == null ? "" : produkt.getEinheit());
|
||||
}
|
||||
|
||||
/** Erst nach dem Vorbefüllen anmelden, damit nur Nutzereingaben zählen. */
|
||||
private void beobachteAenderungen() {
|
||||
DocumentListener listener = new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
};
|
||||
for (JTextField feld : List.of(bezeichnungFeld, beschreibungFeld, preisFeld, einheitFeld)) {
|
||||
feld.getDocument().addDocumentListener(listener);
|
||||
}
|
||||
steuersatzWahl.addActionListener(e -> ungespeichert = true);
|
||||
}
|
||||
|
||||
private void speichere() {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
Produkt produkt = vorhandeneNummer == null
|
||||
? new Produkt()
|
||||
: service.findeProdukt(vorhandeneNummer);
|
||||
produkt.setBezeichnung(bezeichnungFeld.getText().trim());
|
||||
produkt.setBeschreibung(leerZuNull(beschreibungFeld.getText()));
|
||||
produkt.setEinzelpreisNetto(parsePreis(preisFeld.getText()));
|
||||
produkt.setSteuersatz(gewaehlterSteuersatz());
|
||||
produkt.setEinheit(leerZuNull(einheitFeld.getText()));
|
||||
|
||||
Produkt gespeichert = vorhandeneNummer == null
|
||||
? service.legeAn(produkt)
|
||||
: service.aendere(produkt);
|
||||
ungespeichert = false;
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gespeichert. Produktnummer: "
|
||||
+ gespeichert.getProduktnummer()), felder);
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/** Akzeptiert deutsches und englisches Dezimaltrennzeichen. */
|
||||
private BigDecimal parsePreis(String text) {
|
||||
String wert = text.trim().replace(',', '.');
|
||||
if (wert.isEmpty()) {
|
||||
throw new ValidierungsException("Einzelpreis",
|
||||
"Das Pflichtfeld 'Einzelpreis (netto)' fehlt.");
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(wert).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ValidierungsException("Einzelpreis",
|
||||
"Der 'Einzelpreis (netto)' ist keine gültige Zahl: " + text);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal gewaehlterSteuersatz() {
|
||||
return switch (steuersatzWahl.getSelectedIndex()) {
|
||||
case 0 -> new BigDecimal("0.19");
|
||||
case 1 -> new BigDecimal("0.07");
|
||||
default -> new BigDecimal("0.00");
|
||||
};
|
||||
}
|
||||
|
||||
/** Schutz vor Datenverlust: Nachfrage, wenn bereits Eingaben geändert wurden. */
|
||||
private void abbrechenMitNachfrage() {
|
||||
if (ungespeichert) {
|
||||
int antwort = JOptionPane.showConfirmDialog(this,
|
||||
"Die Eingaben gehen verloren. Maske wirklich schließen?",
|
||||
"Eingaben verwerfen", JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispose();
|
||||
}
|
||||
|
||||
private static String leerZuNull(String text) {
|
||||
String wert = text.trim();
|
||||
return wert.isEmpty() ? null : wert;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,70 +2,64 @@ package de.team1.faktura.gui;
|
|||
|
||||
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;
|
||||
import de.team1.faktura.produkte.ProduktVerwaltungsService;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Modulansicht Produktverwaltung (D-F-03 bis F-05): sortierte Liste mit
|
||||
* Suchfeld, Formular mit gekennzeichneten Pflichtfeldern, CSV-Export.
|
||||
* Modulansicht Produktverwaltung (D-F-03 bis F-05): sortierte Liste in voller
|
||||
* Breite mit Suchfeld; Anlegen und Bearbeiten erfolgen über die modale
|
||||
* Formular-Maske {@link ProduktFormularDialog} (Aufruf per Knopf oder
|
||||
* Doppelklick), die Aktionen liegen unter der Tabelle.
|
||||
*/
|
||||
public class ProduktPanel extends JPanel implements ModulPanel {
|
||||
|
||||
private static final String[] STEUERSAETZE = {"19 %", "7 %", "0 %"};
|
||||
|
||||
private final ProduktVerwaltungsService service;
|
||||
private final StammdatenController controller;
|
||||
private final ProduktCsvExport csvExport;
|
||||
|
||||
private final JTextField suchfeld = new JTextField(20);
|
||||
private final ProduktTabellenModel tabellenModel = new ProduktTabellenModel();
|
||||
private final JTable tabelle = new JTable(tabellenModel);
|
||||
private final JLabel trefferAnzeige = UiHilfen.trefferLabel();
|
||||
|
||||
private final JTextField bezeichnungFeld = new JTextField(20);
|
||||
private final JTextField beschreibungFeld = new JTextField(20);
|
||||
private final JTextField preisFeld = new JTextField(10);
|
||||
private final JComboBox<String> steuersatzWahl = new JComboBox<>(STEUERSAETZE);
|
||||
private final JTextField einheitFeld = new JTextField(10);
|
||||
private final JLabel nummerAnzeige = new JLabel("— neues Produkt —");
|
||||
private final Map<String, JComponent> felder = new LinkedHashMap<>();
|
||||
private final JButton bearbeitenKnopf = new JButton("Bearbeiten…");
|
||||
private final JButton loeschenKnopf = new JButton("Löschen");
|
||||
|
||||
private String gewaehlteNummer;
|
||||
private boolean ungespeichert;
|
||||
|
||||
public ProduktPanel(ProduktVerwaltungsService service, ProduktCsvExport csvExport,
|
||||
EreignisBus ereignisBus) {
|
||||
/**
|
||||
* @param service Fachkomponente der Gruppe B für Anlegen/Ändern/Löschen
|
||||
* @param controller GUI-freier Controller für Suche und Listeninhalt (D-F-03)
|
||||
* @param csvExport CSV-Export der Produktstammdaten (B-F-13)
|
||||
* @param ereignisBus Observer-Verteiler für Aktualisierungen nach Datenänderungen
|
||||
*/
|
||||
public ProduktPanel(ProduktVerwaltungsService service, StammdatenController controller,
|
||||
ProduktCsvExport csvExport, EreignisBus ereignisBus) {
|
||||
this.service = service;
|
||||
this.controller = controller;
|
||||
this.csvExport = csvExport;
|
||||
felder.put("Bezeichnung", bezeichnungFeld);
|
||||
felder.put("Einzelpreis", preisFeld);
|
||||
felder.put("Steuersatz", steuersatzWahl);
|
||||
baueOberflaeche();
|
||||
aktualisiere();
|
||||
ereignisBus.abonniere(DatenBereich.PRODUKTE, this::aktualisiere);
|
||||
|
|
@ -73,9 +67,12 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
|
||||
private void baueOberflaeche() {
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
suchleiste.add(new JLabel("Suche (Bezeichnung oder Produktnummer):"));
|
||||
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
|
||||
suchleiste.add(new JLabel("Suche:"));
|
||||
UiHilfen.platzhalter(suchfeld, "Bezeichnung oder Produktnummer…");
|
||||
suchfeld.setToolTipText("Suche nach Bezeichnung oder Produktnummer (Strg+F)");
|
||||
suchleiste.add(suchfeld);
|
||||
suchfeld.getDocument().addDocumentListener(neuerSuchListener());
|
||||
add(suchleiste, BorderLayout.NORTH);
|
||||
|
|
@ -83,191 +80,112 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
TabellenFormat.konfiguriere(tabelle);
|
||||
tabelle.getColumnModel().getColumn(2).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
||||
tabelle.getSelectionModel().addListSelectionListener(e -> {
|
||||
if (!e.getValueIsAdjusting()) {
|
||||
ladeAuswahl();
|
||||
tabelle.getSelectionModel().addListSelectionListener(e -> aktualisiereAktionen());
|
||||
tabelle.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
bearbeite();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
JSplitPane teiler = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
|
||||
new JScrollPane(tabelle), baueFormular());
|
||||
teiler.setResizeWeight(0.55);
|
||||
add(teiler, BorderLayout.CENTER);
|
||||
JPanel listenSeite = new JPanel(new BorderLayout(0, 4));
|
||||
listenSeite.add(new JScrollPane(tabelle), BorderLayout.CENTER);
|
||||
listenSeite.add(trefferAnzeige, BorderLayout.SOUTH);
|
||||
add(listenSeite, BorderLayout.CENTER);
|
||||
|
||||
JPanel aktionen = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JButton neuKnopf = new JButton("Neues Produkt anlegen…");
|
||||
neuKnopf.setMnemonic('N');
|
||||
neuKnopf.setToolTipText("Öffnet die Maske für ein neues Produkt (Alt+N)");
|
||||
neuKnopf.addActionListener(e -> legeNeuAn());
|
||||
bearbeitenKnopf.setMnemonic('B');
|
||||
bearbeitenKnopf.addActionListener(e -> bearbeite());
|
||||
loeschenKnopf.setMnemonic('L');
|
||||
loeschenKnopf.addActionListener(e -> loesche());
|
||||
JButton exportKnopf = new JButton("CSV exportieren…");
|
||||
exportKnopf.setMnemonic('C');
|
||||
exportKnopf.addActionListener(e -> exportiere());
|
||||
aktionen.add(neuKnopf);
|
||||
aktionen.add(bearbeitenKnopf);
|
||||
aktionen.add(loeschenKnopf);
|
||||
aktionen.add(exportKnopf);
|
||||
add(aktionen, BorderLayout.SOUTH);
|
||||
|
||||
UiHilfen.strgF(this, suchfeld);
|
||||
aktualisiereAktionen();
|
||||
}
|
||||
|
||||
private JPanel baueFormular() {
|
||||
JPanel formular = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(3, 3, 3, 3);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
private Produkt auswahl() {
|
||||
int zeile = tabelle.getSelectedRow();
|
||||
return zeile < 0 ? null
|
||||
: tabellenModel.gibZeile(tabelle.convertRowIndexToModel(zeile));
|
||||
}
|
||||
|
||||
int zeile = 0;
|
||||
zeile = formularZeile(formular, c, zeile, "Produktnummer:", nummerAnzeige);
|
||||
zeile = formularZeile(formular, c, zeile, "Bezeichnung: *", bezeichnungFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Beschreibung:", beschreibungFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Einzelpreis (netto): *", preisFeld);
|
||||
zeile = formularZeile(formular, c, zeile, "Steuersatz: *", steuersatzWahl);
|
||||
zeile = formularZeile(formular, c, zeile, "Einheit:", einheitFeld);
|
||||
private void aktualisiereAktionen() {
|
||||
boolean ausgewaehlt = auswahl() != null;
|
||||
String hinweis = "Bitte zuerst ein Produkt in der Liste auswählen.";
|
||||
UiHilfen.schalteAktion(bearbeitenKnopf, ausgewaehlt, hinweis);
|
||||
UiHilfen.schalteAktion(loeschenKnopf, ausgewaehlt, hinweis);
|
||||
}
|
||||
|
||||
DocumentListener aenderungsListener = neuerAenderungsListener();
|
||||
for (JTextField feld : List.of(bezeichnungFeld, beschreibungFeld, preisFeld, einheitFeld)) {
|
||||
feld.getDocument().addDocumentListener(aenderungsListener);
|
||||
private void legeNeuAn() {
|
||||
new ProduktFormularDialog(SwingUtilities.getWindowAncestor(this), service, null)
|
||||
.setVisible(true);
|
||||
}
|
||||
|
||||
private void bearbeite() {
|
||||
Produkt produkt = auswahl();
|
||||
if (produkt == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JButton neu = new JButton("Neu");
|
||||
neu.addActionListener(e -> leereFormular());
|
||||
JButton speichern = new JButton("Speichern");
|
||||
speichern.addActionListener(e -> speichere());
|
||||
JButton loeschen = new JButton("Löschen");
|
||||
loeschen.addActionListener(e -> loesche());
|
||||
JButton export = new JButton("CSV-Export");
|
||||
export.addActionListener(e -> exportiere());
|
||||
knoepfe.add(neu);
|
||||
knoepfe.add(speichern);
|
||||
knoepfe.add(loeschen);
|
||||
knoepfe.add(export);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 2;
|
||||
formular.add(knoepfe, c);
|
||||
return formular;
|
||||
}
|
||||
|
||||
private int formularZeile(JPanel formular, GridBagConstraints c, int zeile,
|
||||
String beschriftung, JComponent feld) {
|
||||
c.gridx = 0;
|
||||
c.gridy = zeile;
|
||||
c.gridwidth = 1;
|
||||
c.weightx = 0;
|
||||
formular.add(new JLabel(beschriftung), c);
|
||||
c.gridx = 1;
|
||||
c.weightx = 1;
|
||||
formular.add(feld, c);
|
||||
return zeile + 1;
|
||||
}
|
||||
|
||||
private void speichere() {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
Produkt produkt = gewaehlteNummer == null
|
||||
? new Produkt()
|
||||
: service.findeProdukt(gewaehlteNummer);
|
||||
produkt.setBezeichnung(bezeichnungFeld.getText().trim());
|
||||
produkt.setBeschreibung(leerZuNull(beschreibungFeld.getText()));
|
||||
produkt.setEinzelpreisNetto(parsePreis(preisFeld.getText()));
|
||||
produkt.setSteuersatz(gewaehlterSteuersatz());
|
||||
produkt.setEinheit(leerZuNull(einheitFeld.getText()));
|
||||
|
||||
Produkt gespeichert = gewaehlteNummer == null
|
||||
? service.legeAn(produkt)
|
||||
: service.aendere(produkt);
|
||||
ungespeichert = false;
|
||||
gewaehlteNummer = gespeichert.getProduktnummer();
|
||||
nummerAnzeige.setText(gespeichert.getProduktnummer());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gespeichert. Produktnummer: "
|
||||
+ gespeichert.getProduktnummer()), felder);
|
||||
});
|
||||
}
|
||||
|
||||
/** Akzeptiert deutsches und englisches Dezimaltrennzeichen. */
|
||||
private BigDecimal parsePreis(String text) {
|
||||
String wert = text.trim().replace(',', '.');
|
||||
if (wert.isEmpty()) {
|
||||
throw new ValidierungsException("Einzelpreis",
|
||||
"Das Pflichtfeld 'Einzelpreis (netto)' fehlt.");
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(wert).setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ValidierungsException("Einzelpreis",
|
||||
"Der 'Einzelpreis (netto)' ist keine gültige Zahl: " + text);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal gewaehlterSteuersatz() {
|
||||
return switch (steuersatzWahl.getSelectedIndex()) {
|
||||
case 0 -> new BigDecimal("0.19");
|
||||
case 1 -> new BigDecimal("0.07");
|
||||
default -> new BigDecimal("0.00");
|
||||
};
|
||||
new ProduktFormularDialog(SwingUtilities.getWindowAncestor(this), service, produkt)
|
||||
.setVisible(true);
|
||||
}
|
||||
|
||||
private void loesche() {
|
||||
if (gewaehlteNummer == null) {
|
||||
Produkt produkt = auswahl();
|
||||
if (produkt == null) {
|
||||
return;
|
||||
}
|
||||
int antwort = JOptionPane.showConfirmDialog(this,
|
||||
"Produkt " + gewaehlteNummer + " wirklich dauerhaft löschen?",
|
||||
"Produkt " + produkt.getProduktnummer() + " wirklich dauerhaft löschen?",
|
||||
"Produkt löschen", JOptionPane.YES_NO_OPTION);
|
||||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
||||
service.loescheProdukt(gewaehlteNummer);
|
||||
leereFormular();
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gelöscht."), felder);
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
service.loescheProdukt(produkt.getProduktnummer());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gelöscht."), null);
|
||||
});
|
||||
}
|
||||
|
||||
private void exportiere() {
|
||||
JFileChooser auswahl = new JFileChooser();
|
||||
auswahl.setSelectedFile(new java.io.File("produkte.csv"));
|
||||
if (auswahl.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
csvExport.exportiereCsv(auswahl.getSelectedFile().toPath());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Die Produktstammdaten wurden exportiert nach "
|
||||
+ auswahl.getSelectedFile()), felder);
|
||||
JFileChooser auswahlDialog = new JFileChooser();
|
||||
auswahlDialog.setSelectedFile(new File("produkte.csv"));
|
||||
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||
csvExport.exportiereCsv(auswahlDialog.getSelectedFile().toPath());
|
||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Die Produktstammdaten wurden exportiert nach "
|
||||
+ auswahlDialog.getSelectedFile()), null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ladeAuswahl() {
|
||||
int zeile = tabelle.getSelectedRow();
|
||||
if (zeile < 0) {
|
||||
return;
|
||||
}
|
||||
Produkt produkt = tabellenModel.produkte.get(tabelle.convertRowIndexToModel(zeile));
|
||||
gewaehlteNummer = produkt.getProduktnummer();
|
||||
nummerAnzeige.setText(produkt.getProduktnummer());
|
||||
bezeichnungFeld.setText(produkt.getBezeichnung());
|
||||
beschreibungFeld.setText(produkt.getBeschreibung() == null ? "" : produkt.getBeschreibung());
|
||||
preisFeld.setText(produkt.getEinzelpreisNetto().toPlainString());
|
||||
steuersatzWahl.setSelectedIndex(switch (produkt.getSteuersatz().stripTrailingZeros().toPlainString()) {
|
||||
case "0.19" -> 0;
|
||||
case "0.07" -> 1;
|
||||
default -> 2;
|
||||
});
|
||||
einheitFeld.setText(produkt.getEinheit() == null ? "" : produkt.getEinheit());
|
||||
ungespeichert = false;
|
||||
}
|
||||
|
||||
private void leereFormular() {
|
||||
gewaehlteNummer = null;
|
||||
nummerAnzeige.setText("— neues Produkt —");
|
||||
for (JTextField feld : List.of(bezeichnungFeld, beschreibungFeld, preisFeld, einheitFeld)) {
|
||||
feld.setText("");
|
||||
}
|
||||
steuersatzWahl.setSelectedIndex(0);
|
||||
tabelle.clearSelection();
|
||||
ungespeichert = false;
|
||||
}
|
||||
|
||||
private static String leerZuNull(String text) {
|
||||
String wert = text.trim();
|
||||
return wert.isEmpty() ? null : wert;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hatUngespeicherteAenderungen() {
|
||||
return ungespeichert;
|
||||
// Eingaben erfolgen ausschließlich in der modalen Formular-Maske;
|
||||
// dort schützt die eigene Nachfrage vor Datenverlust (D-F-02).
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void aktualisiere() {
|
||||
String begriff = suchfeld.getText().trim();
|
||||
tabellenModel.setze(begriff.isEmpty()
|
||||
? service.alleSortiertNachBezeichnung()
|
||||
: service.suche(begriff));
|
||||
List<Produkt> liste = controller.produkteListe(suchfeld.getText());
|
||||
tabellenModel.setze(liste);
|
||||
trefferAnzeige.setText(UiHilfen.trefferText(liste.size(),
|
||||
controller.produkteListe("").size(), "Produkte", suchfeld.getText()));
|
||||
}
|
||||
|
||||
private DocumentListener neuerSuchListener() {
|
||||
|
|
@ -289,25 +207,6 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
};
|
||||
}
|
||||
|
||||
private DocumentListener neuerAenderungsListener() {
|
||||
return new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
ungespeichert = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static final class ProduktTabellenModel extends AbstractTableModel {
|
||||
|
||||
private static final String[] SPALTEN = {"Produktnummer", "Bezeichnung", "Einzelpreis (netto)", "Steuersatz", "Einheit"};
|
||||
|
|
@ -319,6 +218,10 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
|||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
Produkt gibZeile(int zeile) {
|
||||
return produkte.get(zeile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return produkte.size();
|
||||
|
|
|
|||
|
|
@ -5,13 +5,16 @@ import de.team1.faktura.kunden.KundenService;
|
|||
import de.team1.faktura.produkte.Produkt;
|
||||
import de.team1.faktura.produkte.ProduktService;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultComboBoxModel;
|
||||
import javax.swing.DefaultListModel;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSpinner;
|
||||
|
|
@ -19,11 +22,13 @@ import javax.swing.JTextArea;
|
|||
import javax.swing.JTextField;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.Font;
|
||||
import java.awt.Window;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
|
@ -63,6 +68,7 @@ public class RechnungsWizardDialog extends JDialog {
|
|||
private final JButton weiterKnopf = new JButton("Weiter >");
|
||||
private final JButton speichernKnopf = new JButton("Speichern");
|
||||
private final JLabel schrittAnzeige = new JLabel();
|
||||
private final JLabel[] schrittMarkierungen = new JLabel[WizardSchritt.values().length];
|
||||
|
||||
public RechnungsWizardDialog(Window besitzer, RechnungsWizardController controller,
|
||||
KundenService kundenService, ProduktService produktService) {
|
||||
|
|
@ -80,7 +86,22 @@ public class RechnungsWizardDialog extends JDialog {
|
|||
|
||||
private void baueOberflaeche() {
|
||||
setLayout(new BorderLayout(8, 8));
|
||||
add(schrittAnzeige, BorderLayout.NORTH);
|
||||
((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// Schrittindikator: alle fünf Schritte sichtbar, aktueller Schritt hervorgehoben (Q-05)
|
||||
JPanel kopf = new JPanel(new BorderLayout(0, 2));
|
||||
JPanel schritte = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
|
||||
WizardSchritt[] alleSchritte = WizardSchritt.values();
|
||||
for (int i = 0; i < alleSchritte.length; i++) {
|
||||
if (i > 0) {
|
||||
schritte.add(new JLabel("›"));
|
||||
}
|
||||
schrittMarkierungen[i] = new JLabel((i + 1) + ". " + schrittName(alleSchritte[i]));
|
||||
schritte.add(schrittMarkierungen[i]);
|
||||
}
|
||||
kopf.add(schritte, BorderLayout.NORTH);
|
||||
kopf.add(schrittAnzeige, BorderLayout.SOUTH);
|
||||
add(kopf, BorderLayout.NORTH);
|
||||
|
||||
kartenPanel.add(baueSchrittKunde(), WizardSchritt.KUNDE_WAEHLEN.name());
|
||||
kartenPanel.add(baueSchrittPositionen(), WizardSchritt.POSITIONEN_ERFASSEN.name());
|
||||
|
|
@ -91,18 +112,41 @@ public class RechnungsWizardDialog extends JDialog {
|
|||
|
||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
JButton abbrechen = new JButton("Abbrechen");
|
||||
abbrechen.addActionListener(e -> dispose());
|
||||
abbrechen.setMnemonic('A');
|
||||
abbrechen.addActionListener(e -> abbrechenMitNachfrage());
|
||||
zurueckKnopf.setMnemonic('Z');
|
||||
zurueckKnopf.addActionListener(e -> {
|
||||
controller.zurueck();
|
||||
zeigeSchritt();
|
||||
});
|
||||
weiterKnopf.setMnemonic('W');
|
||||
weiterKnopf.addActionListener(e -> weiter());
|
||||
speichernKnopf.setMnemonic('S');
|
||||
speichernKnopf.addActionListener(e -> speichere());
|
||||
knoepfe.add(abbrechen);
|
||||
knoepfe.add(zurueckKnopf);
|
||||
knoepfe.add(weiterKnopf);
|
||||
knoepfe.add(speichernKnopf);
|
||||
add(knoepfe, BorderLayout.SOUTH);
|
||||
|
||||
UiHilfen.escSchliesst(this, this::abbrechenMitNachfrage);
|
||||
UiHilfen.fensterSchliessenAbfangen(this, this::abbrechenMitNachfrage);
|
||||
}
|
||||
|
||||
/** Schutz vor Datenverlust: Nachfrage, wenn bereits Eingaben erfasst wurden. */
|
||||
private void abbrechenMitNachfrage() {
|
||||
boolean datenErfasst = positionsListenModel.getSize() > 0
|
||||
|| kundenListe.getSelectedValue() != null;
|
||||
if (datenErfasst) {
|
||||
int antwort = JOptionPane.showConfirmDialog(this,
|
||||
"Die erfassten Eingaben gehen verloren. Assistent wirklich schließen?",
|
||||
"Eingaben verwerfen", JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
if (antwort != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
dispose();
|
||||
}
|
||||
|
||||
/** Schritt 1: Kunde auswählen (F-09). */
|
||||
|
|
@ -147,6 +191,7 @@ public class RechnungsWizardDialog extends JDialog {
|
|||
eingabe.add(new JLabel("Menge:"));
|
||||
eingabe.add(mengeWahl);
|
||||
JButton hinzufuegen = new JButton("Hinzufügen");
|
||||
hinzufuegen.setMnemonic('H');
|
||||
hinzufuegen.addActionListener(e -> {
|
||||
Produkt produkt = (Produkt) produktWahl.getSelectedItem();
|
||||
if (produkt == null) {
|
||||
|
|
@ -160,6 +205,7 @@ public class RechnungsWizardDialog extends JDialog {
|
|||
});
|
||||
eingabe.add(hinzufuegen);
|
||||
JButton entfernen = new JButton("Entfernen");
|
||||
entfernen.setMnemonic('E');
|
||||
entfernen.addActionListener(e -> {
|
||||
int index = positionsListe.getSelectedIndex();
|
||||
if (index >= 0) {
|
||||
|
|
@ -178,9 +224,12 @@ public class RechnungsWizardDialog extends JDialog {
|
|||
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
panel.add(new JLabel("Rechnungsdatum (TT.MM.JJJJ): *"));
|
||||
rechnungsdatumFeld.setText(DATUM.format(LocalDate.now()));
|
||||
rechnungsdatumFeld.setToolTipText("Pflichtfeld — Format: TT.MM.JJJJ");
|
||||
panel.add(rechnungsdatumFeld);
|
||||
panel.add(new JLabel("Zahlungsziel (leer = 14 Tage):"));
|
||||
zahlungszielFeld.setToolTipText("Optional — Format: TT.MM.JJJJ, leer = 14 Tage nach Rechnungsdatum");
|
||||
panel.add(zahlungszielFeld);
|
||||
panel.add(UiHilfen.pflichtfeldLegende());
|
||||
return panel;
|
||||
}
|
||||
|
||||
|
|
@ -251,10 +300,20 @@ public class RechnungsWizardDialog extends JDialog {
|
|||
zusammenfassung.setText(controller.erzeugeZusammenfassung());
|
||||
}
|
||||
karten.show(kartenPanel, schritt.name());
|
||||
schrittAnzeige.setText(" Schritt " + (schritt.ordinal() + 1) + " von 5: " + schrittName(schritt));
|
||||
schrittAnzeige.setText("Schritt " + (schritt.ordinal() + 1) + " von 5: " + schrittName(schritt));
|
||||
for (int i = 0; i < schrittMarkierungen.length; i++) {
|
||||
boolean aktuell = i == schritt.ordinal();
|
||||
schrittMarkierungen[i].setFont(schrittMarkierungen[i].getFont()
|
||||
.deriveFont(aktuell ? Font.BOLD : Font.PLAIN));
|
||||
schrittMarkierungen[i].setForeground(UIManager.getColor(
|
||||
aktuell ? "Label.foreground" : "Label.disabledForeground"));
|
||||
}
|
||||
zurueckKnopf.setEnabled(schritt.ordinal() > 0);
|
||||
weiterKnopf.setEnabled(schritt != WizardSchritt.SPEICHERN);
|
||||
speichernKnopf.setEnabled(schritt == WizardSchritt.SPEICHERN);
|
||||
// Enter führt immer die sinnvollste Aktion des Schritts aus
|
||||
getRootPane().setDefaultButton(
|
||||
schritt == WizardSchritt.SPEICHERN ? speichernKnopf : weiterKnopf);
|
||||
}
|
||||
|
||||
private static String schrittName(WizardSchritt schritt) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import java.util.List;
|
|||
/**
|
||||
* Controller der Stammdaten-Ansichten (D-F-03): delegiert Suchanfragen an
|
||||
* die Dienste der Gruppen B und C; die GUI rechnet und filtert selbst nicht.
|
||||
* GUI-frei und damit im Modultest ohne Oberfläche prüfbar (TC-14, TC-15).
|
||||
*/
|
||||
public class StammdatenController {
|
||||
|
||||
|
|
@ -21,11 +22,31 @@ public class StammdatenController {
|
|||
this.produktService = produktService;
|
||||
}
|
||||
|
||||
/** Volltextsuche über Name oder Kundennummer (C-F-12). */
|
||||
public List<Kunde> sucheKunden(String suchbegriff) {
|
||||
return kundenService.suche(suchbegriff);
|
||||
}
|
||||
|
||||
/** Volltextsuche über Bezeichnung oder Produktnummer (B-F-12). */
|
||||
public List<Produkt> sucheProdukte(String suchbegriff) {
|
||||
return produktService.suche(suchbegriff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listeninhalt der Kunden-Modulansicht (D-F-03): ein leerer oder fehlender
|
||||
* Suchbegriff zeigt den gesamten Bestand; die Sortierung nach Name kommt
|
||||
* aus der Fachkomponente (C-F-11).
|
||||
*/
|
||||
public List<Kunde> kundenListe(String suchbegriff) {
|
||||
return sucheKunden(suchbegriff == null ? "" : suchbegriff.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Listeninhalt der Produkt-Modulansicht (D-F-03): ein leerer oder fehlender
|
||||
* Suchbegriff zeigt den gesamten Bestand; die Sortierung nach Bezeichnung
|
||||
* kommt aus der Fachkomponente (B-F-11).
|
||||
*/
|
||||
public List<Produkt> produkteListe(String suchbegriff) {
|
||||
return sucheProdukte(suchbegriff == null ? "" : suchbegriff.trim());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public final class TabellenFormat {
|
|||
|
||||
/** Einheitliche Grundeinstellungen: Zeilenhöhe und Sortierung per Spaltenkopf. */
|
||||
public static void konfiguriere(JTable tabelle) {
|
||||
tabelle.setRowHeight(24);
|
||||
tabelle.setRowHeight(26);
|
||||
tabelle.setAutoCreateRowSorter(true);
|
||||
tabelle.setFillsViewportHeight(true);
|
||||
}
|
||||
|
|
@ -66,7 +66,8 @@ public final class TabellenFormat {
|
|||
|
||||
private static Color farbeFuer(DokumentStatus status) {
|
||||
return switch (status) {
|
||||
case ENTWURF -> new Color(110, 110, 110);
|
||||
// dunkleres Grau: Kontrast >= 4,5:1 auch auf der alternierenden Zeilenfarbe
|
||||
case ENTWURF -> new Color(90, 90, 90);
|
||||
case OFFEN -> new Color(0, 90, 180);
|
||||
case VERSENDET -> new Color(0, 130, 60);
|
||||
case STORNIERT -> new Color(180, 40, 40);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
package de.team1.faktura.gui;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.UIManager;
|
||||
import java.awt.Color;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
/**
|
||||
* Gemeinsame Bedienhilfen der Oberfläche: Tastaturkürzel, Platzhaltertexte,
|
||||
* Pflichtfeld-Legende und Trefferanzeige. Unterstützt die einheitliche
|
||||
* Bedienbarkeit aller Modulansichten und Dialoge (Q-05, Q-09).
|
||||
*/
|
||||
public final class UiHilfen {
|
||||
|
||||
private UiHilfen() {
|
||||
}
|
||||
|
||||
/** Grauer Platzhaltertext im leeren Eingabefeld (FlatLaf-Eigenschaft). */
|
||||
public static void platzhalter(JTextField feld, String text) {
|
||||
feld.putClientProperty("JTextField.placeholderText", text);
|
||||
}
|
||||
|
||||
/** ESC führt die Schließ-Aktion des Dialogs aus (inkl. Nachfrage-Logik). */
|
||||
public static void escSchliesst(JDialog dialog, Runnable schliessAktion) {
|
||||
bindeTaste(dialog.getRootPane(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||
"dialogSchliessen", schliessAktion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leitet auch das Schließen über die Fensterleiste (X) auf die
|
||||
* Schließ-Aktion um, damit die Nachfrage nicht umgangen wird.
|
||||
*/
|
||||
public static void fensterSchliessenAbfangen(JDialog dialog, Runnable schliessAktion) {
|
||||
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
|
||||
dialog.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
schliessAktion.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Strg+F setzt den Fokus in das Suchfeld der sichtbaren Modulansicht. */
|
||||
public static void strgF(JComponent panel, JTextField suchfeld) {
|
||||
bindeTaste(panel, KeyStroke.getKeyStroke(KeyEvent.VK_F, KeyEvent.CTRL_DOWN_MASK),
|
||||
"sucheFokussieren", () -> {
|
||||
suchfeld.requestFocusInWindow();
|
||||
suchfeld.selectAll();
|
||||
});
|
||||
}
|
||||
|
||||
/** Strg+S speichert das Formular der sichtbaren Modulansicht. */
|
||||
public static void strgS(JComponent panel, Runnable speichern) {
|
||||
bindeTaste(panel, KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK),
|
||||
"formularSpeichern", speichern);
|
||||
}
|
||||
|
||||
private static void bindeTaste(JComponent komponente, KeyStroke taste,
|
||||
String aktionsName, Runnable aktion) {
|
||||
komponente.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(taste, aktionsName);
|
||||
komponente.getActionMap().put(aktionsName, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
aktion.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Schaltet einen Aktionsknopf und erklärt im Tooltip, warum er gesperrt ist. */
|
||||
public static void schalteAktion(JButton knopf, boolean aktiv, String grundWennGesperrt) {
|
||||
knopf.setEnabled(aktiv);
|
||||
knopf.setToolTipText(aktiv ? null : grundWennGesperrt);
|
||||
}
|
||||
|
||||
/** Kleines graues Hinweis-Label „* Pflichtfeld" unter Formularen (Q-09). */
|
||||
public static JLabel pflichtfeldLegende() {
|
||||
JLabel legende = new JLabel("* Pflichtfeld");
|
||||
legende.setForeground(dezentesGrau());
|
||||
legende.putClientProperty("FlatLaf.styleClass", "small");
|
||||
return legende;
|
||||
}
|
||||
|
||||
/** Graues Label für Trefferanzahl bzw. Leerzustand unter einer Tabelle. */
|
||||
public static JLabel trefferLabel() {
|
||||
JLabel label = new JLabel(" ");
|
||||
label.setForeground(dezentesGrau());
|
||||
label.setBorder(BorderFactory.createEmptyBorder(0, 4, 2, 4));
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text der Trefferanzeige: Anzahl der gezeigten von allen Einträgen,
|
||||
* verständlicher Leerzustand bei erfolgloser Suche oder leerem Bestand.
|
||||
*
|
||||
* @param gezeigt Anzahl der aktuell angezeigten Einträge
|
||||
* @param gesamt Gesamtbestand ohne Filter
|
||||
* @param einheit Mehrzahlbegriff, z. B. "Kunden", "Produkte", "Belege"
|
||||
* @param suchbegriff aktueller Such-/Filtertext (leer = kein Filter)
|
||||
*/
|
||||
public static String trefferText(int gezeigt, int gesamt, String einheit, String suchbegriff) {
|
||||
if (gesamt == 0) {
|
||||
return "Noch keine " + einheit + " vorhanden";
|
||||
}
|
||||
if (gezeigt == 0) {
|
||||
return "Keine Treffer für „" + suchbegriff.trim() + "“";
|
||||
}
|
||||
return gezeigt + " von " + gesamt + " " + einheit;
|
||||
}
|
||||
|
||||
private static Color dezentesGrau() {
|
||||
Color farbe = UIManager.getColor("Label.disabledForeground");
|
||||
return farbe == null ? new Color(110, 110, 110) : farbe;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package de.team1.faktura.dokumente;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* PDF-Export (A-F-04): muss auch Altdaten ohne Produkt-Snapshot verkraften —
|
||||
* Positionen aus früheren Datenbeständen können {@code null} in
|
||||
* Produktreferenz, Einzelpreis und Positionssumme enthalten (IF-01).
|
||||
*/
|
||||
class PdfBoxPdfExporterTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
@DisplayName("Vollständiger Beleg wird als PDF-Datei exportiert")
|
||||
void exportiertVollstaendigenBeleg() {
|
||||
Path ziel = tempDir.resolve("rechnung.pdf");
|
||||
Rechnung rechnung = TestBelege.rechnung("R-2026-000001", DokumentStatus.OFFEN);
|
||||
|
||||
new PdfBoxPdfExporter().exportiere(rechnung, ziel);
|
||||
|
||||
assertTrue(Files.exists(ziel));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Altdaten mit null-Positionsfeldern werfen beim Export keinen Fehler")
|
||||
void exportiertBelegMitNullPositionsfeldern() {
|
||||
Path ziel = tempDir.resolve("altdaten.pdf");
|
||||
Rechnung rechnung = new Rechnung();
|
||||
rechnung.setBelegnummer("R-2026-000099");
|
||||
// Default-Konstruktor wie beim Laden unvollständiger JSON-Altdaten:
|
||||
// produktReferenz, einzelpreisNetto und positionssummeNetto sind null
|
||||
rechnung.setzePositionen(List.of(new Dokumentposition()));
|
||||
|
||||
assertDoesNotThrow(() -> new PdfBoxPdfExporter().exportiere(rechnung, ziel));
|
||||
assertTrue(Files.exists(ziel));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Summenberechnung behandelt null-Positionssummen als 0")
|
||||
void summenberechnungToleriertNullPositionen() {
|
||||
Rechnung rechnung = new Rechnung();
|
||||
rechnung.setBelegnummer("R-2026-000098");
|
||||
|
||||
rechnung.setzePositionen(List.of(new Dokumentposition()));
|
||||
|
||||
assertEquals(new BigDecimal("0.00"), rechnung.getSummeNetto());
|
||||
assertEquals(new BigDecimal("0.00"), rechnung.getSummeSteuer());
|
||||
assertEquals(new BigDecimal("0.00"), rechnung.getSummeBrutto());
|
||||
}
|
||||
}
|
||||
|
|
@ -257,6 +257,21 @@ class OberflaechenControllerTest {
|
|||
assertEquals("K-000017", treffer.get(0).getKundennummer());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-15: kundenListe zeigt bei leerem Suchbegriff den Bestand und filtert sonst (D-F-03)")
|
||||
void tc15KundenListe() {
|
||||
StammdatenController controller = new StammdatenController(kundenService, produktService);
|
||||
|
||||
// leerer oder fehlender Suchbegriff: gesamter Bestand
|
||||
assertEquals(1, controller.kundenListe("").size());
|
||||
assertEquals(1, controller.kundenListe(" ").size());
|
||||
assertEquals(1, controller.kundenListe(null).size());
|
||||
|
||||
// Suchbegriff filtert (Teilstring über den Namen)
|
||||
assertEquals(1, controller.kundenListe("Muster").size());
|
||||
assertEquals(0, controller.kundenListe("unbekannt").size());
|
||||
}
|
||||
|
||||
/** Zähl-Stub des DokumentService (Gruppe A) für die Controller-Tests. */
|
||||
private static final class DokumentServiceStub implements DokumentService {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue