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.HauptFenster;
|
||||||
import de.team1.faktura.gui.KundenPanel;
|
import de.team1.faktura.gui.KundenPanel;
|
||||||
import de.team1.faktura.gui.ProduktPanel;
|
import de.team1.faktura.gui.ProduktPanel;
|
||||||
|
import de.team1.faktura.gui.StammdatenController;
|
||||||
import de.team1.faktura.kunden.EinfacherKundennummernGenerator;
|
import de.team1.faktura.kunden.EinfacherKundennummernGenerator;
|
||||||
import de.team1.faktura.kunden.JsonKundenRepository;
|
import de.team1.faktura.kunden.JsonKundenRepository;
|
||||||
import de.team1.faktura.kunden.KundenCsvExport;
|
import de.team1.faktura.kunden.KundenCsvExport;
|
||||||
|
|
@ -78,6 +79,10 @@ public final class Main {
|
||||||
new PdfBoxPdfExporter(),
|
new PdfBoxPdfExporter(),
|
||||||
ereignisBus);
|
ereignisBus);
|
||||||
|
|
||||||
|
// Gruppe D — GUI-freier Controller der Stammdaten-Ansichten (D-F-03)
|
||||||
|
StammdatenController stammdatenController =
|
||||||
|
new StammdatenController(kundenService, produktService);
|
||||||
|
|
||||||
// Gruppe D — Programmoberfläche
|
// Gruppe D — Programmoberfläche
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
try {
|
try {
|
||||||
|
|
@ -88,10 +93,10 @@ public final class Main {
|
||||||
// Standard-Look-and-Feel verwenden
|
// Standard-Look-and-Feel verwenden
|
||||||
}
|
}
|
||||||
HauptFenster fenster = new HauptFenster(
|
HauptFenster fenster = new HauptFenster(
|
||||||
new KundenPanel(kundenService, new KundenCsvExport(kundenRepository),
|
new KundenPanel(kundenService, stammdatenController,
|
||||||
ereignisBus),
|
new KundenCsvExport(kundenRepository), ereignisBus),
|
||||||
new ProduktPanel(produktService, new ProduktCsvExport(produktRepository),
|
new ProduktPanel(produktService, stammdatenController,
|
||||||
ereignisBus),
|
new ProduktCsvExport(produktRepository), ereignisBus),
|
||||||
new DokumentListenPanel(dokumentService, kundenService, produktService,
|
new DokumentListenPanel(dokumentService, kundenService, produktService,
|
||||||
new DokumentCsvExport(dokumentRepository), ereignisBus));
|
new DokumentCsvExport(dokumentRepository), ereignisBus));
|
||||||
fenster.setVisible(true);
|
fenster.setVisible(true);
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,10 @@ public abstract class Dokument {
|
||||||
BigDecimal netto = BigDecimal.ZERO;
|
BigDecimal netto = BigDecimal.ZERO;
|
||||||
BigDecimal steuer = BigDecimal.ZERO;
|
BigDecimal steuer = BigDecimal.ZERO;
|
||||||
for (Dokumentposition position : positionen) {
|
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());
|
steuer = steuer.add(position.getSteuerbetrag());
|
||||||
}
|
}
|
||||||
this.summeNetto = netto.setScale(2, RoundingMode.HALF_UP);
|
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). */
|
/** Steuerbetrag der Position: {@code positionssummeNetto * steuersatz}, Scale 2 (F-23, TC-01). */
|
||||||
public BigDecimal getSteuerbetrag() {
|
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);
|
return positionssummeNetto.multiply(steuersatz).setScale(2, RoundingMode.HALF_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,48 @@ import java.io.UncheckedIOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
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).
|
* Standardisierter PDF-Export der Belege mit Apache PDFBox
|
||||||
* Rechnungen enthalten die Pflichtangaben gemäß § 14 UStG (F-13):
|
* (A-F-04, F-07, F-10, F-15), angelehnt an den deutschen Geschäftsbrief:
|
||||||
* Belegnummer, Rechnungs- und Leistungsdatum, Kundendaten, Positionen mit
|
* Absender- und Empfängerblock, Belegkopf mit Datum und Referenzen,
|
||||||
* Einzel- und Gesamtbeträgen, Steuersatz/-betrag sowie die Summen.
|
* 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 {
|
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");
|
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 RAND = 50;
|
||||||
private static final float ZEILENHOEHE = 14;
|
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 normal = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||||
private final PDFont fett = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD);
|
private final PDFont fett = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD);
|
||||||
|
|
@ -35,48 +64,11 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
||||||
try (PDDocument pdf = new PDDocument()) {
|
try (PDDocument pdf = new PDDocument()) {
|
||||||
Schreiber schreiber = new Schreiber(pdf);
|
Schreiber schreiber = new Schreiber(pdf);
|
||||||
|
|
||||||
schreiber.zeile(fett, 16, dokument.belegtyp().anzeigename() + " " + dokument.getBelegnummer());
|
schreibeBriefkopf(schreiber, dokument);
|
||||||
if (dokument.getStatus() == DokumentStatus.STORNIERT) {
|
schreibeBelegkopf(schreiber, dokument);
|
||||||
schreiber.zeile(fett, 12, "*** STORNIERT ***");
|
schreibePositionstabelle(schreiber, dokument);
|
||||||
}
|
schreibeSummenblock(schreiber, dokument);
|
||||||
schreiber.leer();
|
schreibeSchlusstext(schreiber, dokument);
|
||||||
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");
|
|
||||||
|
|
||||||
schreiber.schliesse();
|
schreiber.schliesse();
|
||||||
if (zielDatei.getParent() != null) {
|
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);
|
return datum == null ? "—" : DATUM.format(datum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Deutsches Betragsformat mit Tausenderpunkt, z. B. "1.234,56". */
|
||||||
private static String betrag(BigDecimal wert) {
|
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) {
|
private static String prozent(BigDecimal steuersatz) {
|
||||||
|
if (steuersatz == null) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
return steuersatz.multiply(new BigDecimal("100")).stripTrailingZeros().toPlainString();
|
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) + "…";
|
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 final PDDocument pdf;
|
||||||
private PDPageContentStream inhalt;
|
private PDPageContentStream inhalt;
|
||||||
|
|
@ -119,7 +241,7 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
||||||
neueSeite();
|
neueSeite();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void neueSeite() throws IOException {
|
void neueSeite() throws IOException {
|
||||||
if (inhalt != null) {
|
if (inhalt != null) {
|
||||||
inhalt.close();
|
inhalt.close();
|
||||||
}
|
}
|
||||||
|
|
@ -129,16 +251,59 @@ public class PdfBoxPdfExporter implements PdfExporter {
|
||||||
y = PDRectangle.A4.getHeight() - RAND;
|
y = PDRectangle.A4.getHeight() - RAND;
|
||||||
}
|
}
|
||||||
|
|
||||||
void zeile(PDFont font, float groesse, String text) throws IOException {
|
boolean passtNochZeile() {
|
||||||
if (y < RAND + ZEILENHOEHE) {
|
return y >= RAND + ZEILENHOEHE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Beginnt eine Tabellenzeile; bricht bei Bedarf auf eine neue Seite um. */
|
||||||
|
void beginneZeile() throws IOException {
|
||||||
|
if (!passtNochZeile()) {
|
||||||
neueSeite();
|
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.beginText();
|
||||||
inhalt.setFont(font, groesse);
|
inhalt.setFont(font, groesse);
|
||||||
inhalt.newLineAtOffset(RAND, y);
|
inhalt.newLineAtOffset(x, y);
|
||||||
inhalt.showText(text);
|
inhalt.showText(text == null ? "" : text);
|
||||||
inhalt.endText();
|
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() {
|
void leer() {
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,16 @@ import de.team1.faktura.kunden.KundenService;
|
||||||
import de.team1.faktura.produkte.Produkt;
|
import de.team1.faktura.produkte.Produkt;
|
||||||
import de.team1.faktura.produkte.ProduktService;
|
import de.team1.faktura.produkte.ProduktService;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.DefaultComboBoxModel;
|
import javax.swing.DefaultComboBoxModel;
|
||||||
import javax.swing.DefaultListModel;
|
import javax.swing.DefaultListModel;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JComponent;
|
||||||
import javax.swing.JDialog;
|
import javax.swing.JDialog;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JList;
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JSpinner;
|
import javax.swing.JSpinner;
|
||||||
|
|
@ -74,10 +77,11 @@ public class BelegDialog extends JDialog {
|
||||||
|
|
||||||
private void baueOberflaeche() {
|
private void baueOberflaeche() {
|
||||||
setLayout(new BorderLayout(8, 8));
|
setLayout(new BorderLayout(8, 8));
|
||||||
|
((JComponent) getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||||
|
|
||||||
JPanel kopf = new JPanel(new GridBagLayout());
|
JPanel kopf = new JPanel(new GridBagLayout());
|
||||||
GridBagConstraints c = new GridBagConstraints();
|
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.anchor = GridBagConstraints.WEST;
|
||||||
c.fill = GridBagConstraints.HORIZONTAL;
|
c.fill = GridBagConstraints.HORIZONTAL;
|
||||||
|
|
||||||
|
|
@ -98,7 +102,13 @@ public class BelegDialog extends JDialog {
|
||||||
c.gridy = 2;
|
c.gridy = 2;
|
||||||
kopf.add(datumBeschriftung, c);
|
kopf.add(datumBeschriftung, c);
|
||||||
c.gridx = 1;
|
c.gridx = 1;
|
||||||
|
datumFeld.setToolTipText("Optional — Format: TT.MM.JJJJ");
|
||||||
kopf.add(datumFeld, c);
|
kopf.add(datumFeld, c);
|
||||||
|
|
||||||
|
c.gridx = 0;
|
||||||
|
c.gridy = 3;
|
||||||
|
c.gridwidth = 2;
|
||||||
|
kopf.add(UiHilfen.pflichtfeldLegende(), c);
|
||||||
add(kopf, BorderLayout.NORTH);
|
add(kopf, BorderLayout.NORTH);
|
||||||
|
|
||||||
JPanel mitte = new JPanel(new BorderLayout(5, 5));
|
JPanel mitte = new JPanel(new BorderLayout(5, 5));
|
||||||
|
|
@ -108,9 +118,11 @@ public class BelegDialog extends JDialog {
|
||||||
eingabe.add(new JLabel("Menge:"));
|
eingabe.add(new JLabel("Menge:"));
|
||||||
eingabe.add(mengeWahl);
|
eingabe.add(mengeWahl);
|
||||||
JButton hinzufuegen = new JButton("Hinzufügen");
|
JButton hinzufuegen = new JButton("Hinzufügen");
|
||||||
|
hinzufuegen.setMnemonic('H');
|
||||||
hinzufuegen.addActionListener(e -> fuegePositionHinzu());
|
hinzufuegen.addActionListener(e -> fuegePositionHinzu());
|
||||||
eingabe.add(hinzufuegen);
|
eingabe.add(hinzufuegen);
|
||||||
JButton entfernen = new JButton("Entfernen");
|
JButton entfernen = new JButton("Entfernen");
|
||||||
|
entfernen.setMnemonic('N');
|
||||||
entfernen.addActionListener(e -> entfernePosition());
|
entfernen.addActionListener(e -> entfernePosition());
|
||||||
eingabe.add(entfernen);
|
eingabe.add(entfernen);
|
||||||
mitte.add(eingabe, BorderLayout.NORTH);
|
mitte.add(eingabe, BorderLayout.NORTH);
|
||||||
|
|
@ -119,12 +131,32 @@ public class BelegDialog extends JDialog {
|
||||||
|
|
||||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
JButton abbrechen = new JButton("Abbrechen");
|
JButton abbrechen = new JButton("Abbrechen");
|
||||||
abbrechen.addActionListener(e -> dispose());
|
abbrechen.setMnemonic('A');
|
||||||
|
abbrechen.addActionListener(e -> abbrechenMitNachfrage());
|
||||||
JButton erstellen = new JButton("Erstellen");
|
JButton erstellen = new JButton("Erstellen");
|
||||||
|
erstellen.setMnemonic('E');
|
||||||
erstellen.addActionListener(e -> erstelle());
|
erstellen.addActionListener(e -> erstelle());
|
||||||
knoepfe.add(abbrechen);
|
knoepfe.add(abbrechen);
|
||||||
knoepfe.add(erstellen);
|
knoepfe.add(erstellen);
|
||||||
add(knoepfe, BorderLayout.SOUTH);
|
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() {
|
private void aktualisiereDatumsfeld() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package de.team1.faktura.gui;
|
package de.team1.faktura.gui;
|
||||||
|
|
||||||
|
import de.team1.faktura.dokumente.Belegtyp;
|
||||||
import de.team1.faktura.dokumente.Dokument;
|
import de.team1.faktura.dokumente.Dokument;
|
||||||
import de.team1.faktura.dokumente.DokumentCsvExport;
|
import de.team1.faktura.dokumente.DokumentCsvExport;
|
||||||
import de.team1.faktura.dokumente.DokumentService;
|
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.kunden.KundenService;
|
||||||
import de.team1.faktura.produkte.ProduktService;
|
import de.team1.faktura.produkte.ProduktService;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.DefaultListCellRenderer;
|
import javax.swing.DefaultListCellRenderer;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
|
|
@ -27,6 +29,7 @@ import java.awt.Component;
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
import java.awt.FlowLayout;
|
import java.awt.FlowLayout;
|
||||||
import java.awt.Window;
|
import java.awt.Window;
|
||||||
|
import java.io.File;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
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 folgebelegKnopf = new JButton("Folgebeleg erzeugen");
|
||||||
private final JButton versendenKnopf = new JButton("Versenden");
|
private final JButton versendenKnopf = new JButton("Versenden");
|
||||||
private final JButton stornierenKnopf = new JButton("Stornieren");
|
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 druckenKnopf = new JButton("Drucken");
|
||||||
private final JButton mailKnopf = new JButton("Per E-Mail senden");
|
private final JButton mailKnopf = new JButton("Per E-Mail senden");
|
||||||
|
private final JLabel trefferAnzeige = UiHilfen.trefferLabel();
|
||||||
|
|
||||||
public DokumentListenPanel(DokumentService dokumentService,
|
public DokumentListenPanel(DokumentService dokumentService,
|
||||||
KundenService kundenService,
|
KundenService kundenService,
|
||||||
|
|
@ -84,8 +88,9 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
|
|
||||||
private void baueOberflaeche() {
|
private void baueOberflaeche() {
|
||||||
setLayout(new BorderLayout(8, 8));
|
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(new JLabel("Statusfilter:"));
|
||||||
kopf.add(statusFilter);
|
kopf.add(statusFilter);
|
||||||
statusFilter.setRenderer(new DefaultListCellRenderer() {
|
statusFilter.setRenderer(new DefaultListCellRenderer() {
|
||||||
|
|
@ -99,10 +104,16 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
statusFilter.addActionListener(e -> aktualisiere());
|
statusFilter.addActionListener(e -> aktualisiere());
|
||||||
|
|
||||||
JButton neueRechnung = new JButton("Neue Rechnung (Assistent)…");
|
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());
|
neueRechnung.addActionListener(e -> oeffneWizard());
|
||||||
JButton neuerBeleg = new JButton("Neuer Beleg…");
|
JButton neuerBeleg = new JButton("Neuer Beleg…");
|
||||||
|
neuerBeleg.setMnemonic('B');
|
||||||
|
neuerBeleg.setToolTipText("Angebot, Auftragsbestätigung oder Lieferschein erstellen (Alt+B)");
|
||||||
neuerBeleg.addActionListener(e -> oeffneBelegDialog());
|
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());
|
datenExportKnopf.addActionListener(e -> exportiereDaten());
|
||||||
kopf.add(neueRechnung);
|
kopf.add(neueRechnung);
|
||||||
kopf.add(neuerBeleg);
|
kopf.add(neuerBeleg);
|
||||||
|
|
@ -114,9 +125,18 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
TabellenFormat.konfiguriere(tabelle);
|
TabellenFormat.konfiguriere(tabelle);
|
||||||
tabelle.getColumnModel().getColumn(4).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
tabelle.getColumnModel().getColumn(4).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
||||||
tabelle.getColumnModel().getColumn(5).setCellRenderer(TabellenFormat.statusRenderer());
|
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));
|
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());
|
folgebelegKnopf.addActionListener(e -> erzeugeFolgebeleg());
|
||||||
versendenKnopf.addActionListener(e -> versende());
|
versendenKnopf.addActionListener(e -> versende());
|
||||||
stornierenKnopf.addActionListener(e -> storniere());
|
stornierenKnopf.addActionListener(e -> storniere());
|
||||||
|
|
@ -136,7 +156,7 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
private Dokument auswahl() {
|
private Dokument auswahl() {
|
||||||
int zeile = tabelle.getSelectedRow();
|
int zeile = tabelle.getSelectedRow();
|
||||||
return zeile < 0 ? null
|
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). */
|
/** Aktiviert/deaktiviert die Belegaktionen gemäß Status (F-08, F-14). */
|
||||||
|
|
@ -145,17 +165,22 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
if (dokument == null) {
|
if (dokument == null) {
|
||||||
for (JButton knopf : List.of(folgebelegKnopf, versendenKnopf, stornierenKnopf,
|
for (JButton knopf : List.of(folgebelegKnopf, versendenKnopf, stornierenKnopf,
|
||||||
pdfKnopf, druckenKnopf, mailKnopf)) {
|
pdfKnopf, druckenKnopf, mailKnopf)) {
|
||||||
knopf.setEnabled(false);
|
UiHilfen.schalteAktion(knopf, false, "Bitte zuerst einen Beleg in der Liste auswählen.");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BelegAktionen verfuegbar = controller.aktionenFuer(dokument);
|
BelegAktionen verfuegbar = controller.aktionenFuer(dokument);
|
||||||
folgebelegKnopf.setEnabled(dokument.belegtyp() != de.team1.faktura.dokumente.Belegtyp.RECHNUNG);
|
DokumentStatus status = dokument.getStatus();
|
||||||
versendenKnopf.setEnabled(verfuegbar.aenderbar());
|
UiHilfen.schalteAktion(folgebelegKnopf, dokument.belegtyp() != Belegtyp.RECHNUNG,
|
||||||
stornierenKnopf.setEnabled(verfuegbar.stornierbar());
|
"Für Rechnungen wird kein Folgebeleg erzeugt.");
|
||||||
pdfKnopf.setEnabled(verfuegbar.pdfExport());
|
UiHilfen.schalteAktion(versendenKnopf, verfuegbar.aenderbar(),
|
||||||
druckenKnopf.setEnabled(verfuegbar.pdfExport());
|
"Im Status " + status + " sind keine inhaltlichen Änderungen mehr möglich (GR-02).");
|
||||||
mailKnopf.setEnabled(verfuegbar.pdfExport());
|
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() {
|
private void oeffneWizard() {
|
||||||
|
|
@ -226,23 +251,27 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
JFileChooser auswahlDialog = new JFileChooser();
|
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) {
|
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
Path ziel = auswahlDialog.getSelectedFile().toPath();
|
Path ziel = auswahlDialog.getSelectedFile().toPath();
|
||||||
dokumentService.exportierePdf(dokument.getBelegnummer(), ziel);
|
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das PDF wurde exportiert nach " + ziel), 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). */
|
/** Vollständiger Datenexport aller Belege als CSV (Q-08, IF-04). */
|
||||||
private void exportiereDaten() {
|
private void exportiereDaten() {
|
||||||
JFileChooser auswahlDialog = new JFileChooser();
|
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) {
|
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
Path ziel = auswahlDialog.getSelectedFile().toPath();
|
Path ziel = auswahlDialog.getSelectedFile().toPath();
|
||||||
datenExport.exportiereCsv(ziel);
|
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||||
MeldungsAnzeige.zeige(this, Meldung.erfolg(
|
datenExport.exportiereCsv(ziel);
|
||||||
"Die Belegdaten wurden exportiert nach " + ziel), null);
|
MeldungsAnzeige.zeige(this, Meldung.erfolg(
|
||||||
|
"Die Belegdaten wurden exportiert nach " + ziel), null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,7 +318,17 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void aktualisiere() {
|
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();
|
aktualisiereAktionen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -313,6 +352,10 @@ public class DokumentListenPanel extends JPanel implements ModulPanel {
|
||||||
fireTableDataChanged();
|
fireTableDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dokument gibZeile(int zeile) {
|
||||||
|
return dokumente.get(zeile);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRowCount() {
|
public int getRowCount() {
|
||||||
return dokumente.size();
|
return dokumente.size();
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,35 @@
|
||||||
package de.team1.faktura.gui;
|
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.JFrame;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
import javax.swing.JToolBar;
|
import javax.swing.JToolBar;
|
||||||
|
import javax.swing.KeyStroke;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.CardLayout;
|
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.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hauptfenster mit Navigation zu den drei Modulen Kundenverwaltung,
|
* Hauptfenster mit Navigation zu den drei Modulen Kundenverwaltung,
|
||||||
* Produktverwaltung und Dokumente (D-F-01). Beim Modulwechsel wird bei
|
* Produktverwaltung und Dokumente (D-F-01). Das aktive Modul ist in der
|
||||||
* ungespeicherten Formulareingaben nachgefragt (D-F-02).
|
* Navigationsleiste sichtbar markiert; Alt+1/2/3 wechseln direkt. Beim
|
||||||
|
* Modulwechsel wird bei ungespeicherten Formulareingaben nachgefragt (D-F-02).
|
||||||
*/
|
*/
|
||||||
public class HauptFenster extends JFrame {
|
public class HauptFenster extends JFrame {
|
||||||
|
|
||||||
private final CardLayout karten = new CardLayout();
|
private final CardLayout karten = new CardLayout();
|
||||||
private final JPanel kartenPanel = new JPanel(karten);
|
private final JPanel kartenPanel = new JPanel(karten);
|
||||||
private final Map<String, ModulPanel> module = new LinkedHashMap<>();
|
private final Map<String, ModulPanel> module = new LinkedHashMap<>();
|
||||||
|
private final Map<String, JToggleButton> navigationsKnoepfe = new LinkedHashMap<>();
|
||||||
|
|
||||||
private String aktuellesModul;
|
private String aktuellesModul;
|
||||||
|
|
||||||
|
|
@ -35,10 +45,18 @@ public class HauptFenster extends JFrame {
|
||||||
|
|
||||||
JToolBar navigation = new JToolBar();
|
JToolBar navigation = new JToolBar();
|
||||||
navigation.setFloatable(false);
|
navigation.setFloatable(false);
|
||||||
|
ButtonGroup gruppe = new ButtonGroup();
|
||||||
|
int modulNummer = 1;
|
||||||
for (Map.Entry<String, ModulPanel> eintrag : module.entrySet()) {
|
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()));
|
knopf.addActionListener(e -> wechsleZu(eintrag.getKey()));
|
||||||
|
gruppe.add(knopf);
|
||||||
navigation.add(knopf);
|
navigation.add(knopf);
|
||||||
|
navigationsKnoepfe.put(eintrag.getKey(), knopf);
|
||||||
|
bindeModulTaste(eintrag.getKey(), modulNummer);
|
||||||
|
modulNummer++;
|
||||||
}
|
}
|
||||||
add(navigation, BorderLayout.NORTH);
|
add(navigation, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
|
@ -48,15 +66,29 @@ public class HauptFenster extends JFrame {
|
||||||
add(kartenPanel, BorderLayout.CENTER);
|
add(kartenPanel, BorderLayout.CENTER);
|
||||||
|
|
||||||
aktuellesModul = "Kunden";
|
aktuellesModul = "Kunden";
|
||||||
|
navigationsKnoepfe.get(aktuellesModul).setSelected(true);
|
||||||
karten.show(kartenPanel, aktuellesModul);
|
karten.show(kartenPanel, aktuellesModul);
|
||||||
|
|
||||||
setSize(1100, 650);
|
setSize(1100, 650);
|
||||||
setLocationRelativeTo(null);
|
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). */
|
/** Modulwechsel mit Nachfrage bei ungespeicherten Eingaben (D-F-02). */
|
||||||
private void wechsleZu(String modulName) {
|
private void wechsleZu(String modulName) {
|
||||||
if (modulName.equals(aktuellesModul)) {
|
if (modulName.equals(aktuellesModul)) {
|
||||||
|
navigationsKnoepfe.get(aktuellesModul).setSelected(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ModulPanel aktuell = module.get(aktuellesModul);
|
ModulPanel aktuell = module.get(aktuellesModul);
|
||||||
|
|
@ -66,10 +98,13 @@ public class HauptFenster extends JFrame {
|
||||||
"Ungespeicherte Eingaben", JOptionPane.YES_NO_OPTION,
|
"Ungespeicherte Eingaben", JOptionPane.YES_NO_OPTION,
|
||||||
JOptionPane.WARNING_MESSAGE);
|
JOptionPane.WARNING_MESSAGE);
|
||||||
if (antwort != JOptionPane.YES_OPTION) {
|
if (antwort != JOptionPane.YES_OPTION) {
|
||||||
|
// Wechsel abgebrochen: Markierung zurück auf das aktuelle Modul
|
||||||
|
navigationsKnoepfe.get(aktuellesModul).setSelected(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
aktuellesModul = modulName;
|
aktuellesModul = modulName;
|
||||||
|
navigationsKnoepfe.get(modulName).setSelected(true);
|
||||||
module.get(modulName).aktualisiere();
|
module.get(modulName).aktualisiere();
|
||||||
karten.show(kartenPanel, modulName);
|
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.KundenCsvExport;
|
||||||
import de.team1.faktura.kunden.KundenVerwaltungsService;
|
import de.team1.faktura.kunden.KundenVerwaltungsService;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComponent;
|
|
||||||
import javax.swing.JFileChooser;
|
import javax.swing.JFileChooser;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JSplitPane;
|
|
||||||
import javax.swing.JTable;
|
import javax.swing.JTable;
|
||||||
import javax.swing.JTextField;
|
import javax.swing.JTextField;
|
||||||
import javax.swing.ListSelectionModel;
|
import javax.swing.ListSelectionModel;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.event.DocumentEvent;
|
import javax.swing.event.DocumentEvent;
|
||||||
import javax.swing.event.DocumentListener;
|
import javax.swing.event.DocumentListener;
|
||||||
import javax.swing.table.AbstractTableModel;
|
import javax.swing.table.AbstractTableModel;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.FlowLayout;
|
import java.awt.FlowLayout;
|
||||||
import java.awt.GridBagConstraints;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.GridBagLayout;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.Insets;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modulansicht Kundenverwaltung (D-F-03 bis F-05): sortierte Liste mit
|
* Modulansicht Kundenverwaltung (D-F-03 bis F-05): sortierte Liste in voller
|
||||||
* Suchfeld, Formular mit gekennzeichneten Pflichtfeldern, CSV-Export.
|
* 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 {
|
public class KundenPanel extends JPanel implements ModulPanel {
|
||||||
|
|
||||||
private final KundenVerwaltungsService service;
|
private final KundenVerwaltungsService service;
|
||||||
|
private final StammdatenController controller;
|
||||||
private final KundenCsvExport csvExport;
|
private final KundenCsvExport csvExport;
|
||||||
|
|
||||||
private final JTextField suchfeld = new JTextField(20);
|
private final JTextField suchfeld = new JTextField(20);
|
||||||
private final KundenTabellenModel tabellenModel = new KundenTabellenModel();
|
private final KundenTabellenModel tabellenModel = new KundenTabellenModel();
|
||||||
private final JTable tabelle = new JTable(tabellenModel);
|
private final JTable tabelle = new JTable(tabellenModel);
|
||||||
|
private final JLabel trefferAnzeige = UiHilfen.trefferLabel();
|
||||||
|
|
||||||
private final JTextField nameFeld = new JTextField(20);
|
private final JButton bearbeitenKnopf = new JButton("Bearbeiten…");
|
||||||
private final JTextField strasseFeld = new JTextField(20);
|
private final JButton loeschenKnopf = new JButton("Löschen");
|
||||||
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 String gewaehlteNummer;
|
/**
|
||||||
private boolean ungespeichert;
|
* @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)
|
||||||
public KundenPanel(KundenVerwaltungsService service, KundenCsvExport csvExport,
|
* @param csvExport CSV-Export der Kundenstammdaten (C-F-13)
|
||||||
EreignisBus ereignisBus) {
|
* @param ereignisBus Observer-Verteiler für Aktualisierungen nach Datenänderungen
|
||||||
|
*/
|
||||||
|
public KundenPanel(KundenVerwaltungsService service, StammdatenController controller,
|
||||||
|
KundenCsvExport csvExport, EreignisBus ereignisBus) {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
|
this.controller = controller;
|
||||||
this.csvExport = csvExport;
|
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();
|
baueOberflaeche();
|
||||||
aktualisiere();
|
aktualisiere();
|
||||||
ereignisBus.abonniere(DatenBereich.KUNDEN, this::aktualisiere);
|
ereignisBus.abonniere(DatenBereich.KUNDEN, this::aktualisiere);
|
||||||
|
|
@ -72,180 +66,124 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
||||||
|
|
||||||
private void baueOberflaeche() {
|
private void baueOberflaeche() {
|
||||||
setLayout(new BorderLayout(8, 8));
|
setLayout(new BorderLayout(8, 8));
|
||||||
|
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||||
|
|
||||||
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
|
||||||
suchleiste.add(new JLabel("Suche (Name oder Kundennummer):"));
|
suchleiste.add(new JLabel("Suche:"));
|
||||||
|
UiHilfen.platzhalter(suchfeld, "Name oder Kundennummer…");
|
||||||
|
suchfeld.setToolTipText("Suche nach Name oder Kundennummer (Strg+F)");
|
||||||
suchleiste.add(suchfeld);
|
suchleiste.add(suchfeld);
|
||||||
suchfeld.getDocument().addDocumentListener(neuerSuchListener());
|
suchfeld.getDocument().addDocumentListener(neuerSuchListener());
|
||||||
add(suchleiste, BorderLayout.NORTH);
|
add(suchleiste, BorderLayout.NORTH);
|
||||||
|
|
||||||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
TabellenFormat.konfiguriere(tabelle);
|
TabellenFormat.konfiguriere(tabelle);
|
||||||
tabelle.getSelectionModel().addListSelectionListener(e -> {
|
tabelle.getSelectionModel().addListSelectionListener(e -> aktualisiereAktionen());
|
||||||
if (!e.getValueIsAdjusting()) {
|
tabelle.addMouseListener(new MouseAdapter() {
|
||||||
ladeAuswahl();
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.getClickCount() == 2) {
|
||||||
|
bearbeite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
JSplitPane teiler = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
|
JPanel listenSeite = new JPanel(new BorderLayout(0, 4));
|
||||||
new JScrollPane(tabelle), baueFormular());
|
listenSeite.add(new JScrollPane(tabelle), BorderLayout.CENTER);
|
||||||
teiler.setResizeWeight(0.55);
|
listenSeite.add(trefferAnzeige, BorderLayout.SOUTH);
|
||||||
add(teiler, BorderLayout.CENTER);
|
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() {
|
private Kunde auswahl() {
|
||||||
JPanel formular = new JPanel(new GridBagLayout());
|
int zeile = tabelle.getSelectedRow();
|
||||||
GridBagConstraints c = new GridBagConstraints();
|
return zeile < 0 ? null
|
||||||
c.insets = new Insets(3, 3, 3, 3);
|
: tabellenModel.gibZeile(tabelle.convertRowIndexToModel(zeile));
|
||||||
c.anchor = GridBagConstraints.WEST;
|
}
|
||||||
c.fill = GridBagConstraints.HORIZONTAL;
|
|
||||||
|
|
||||||
int zeile = 0;
|
private void aktualisiereAktionen() {
|
||||||
zeile = formularZeile(formular, c, zeile, "Kundennummer:", nummerAnzeige);
|
boolean ausgewaehlt = auswahl() != null;
|
||||||
zeile = formularZeile(formular, c, zeile, "Name: *", nameFeld);
|
String hinweis = "Bitte zuerst einen Kunden in der Liste auswählen.";
|
||||||
zeile = formularZeile(formular, c, zeile, "Straße: *", strasseFeld);
|
UiHilfen.schalteAktion(bearbeitenKnopf, ausgewaehlt, hinweis);
|
||||||
zeile = formularZeile(formular, c, zeile, "PLZ: *", plzFeld);
|
UiHilfen.schalteAktion(loeschenKnopf, ausgewaehlt, hinweis);
|
||||||
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);
|
|
||||||
|
|
||||||
DocumentListener aenderungsListener = neuerAenderungsListener();
|
private void legeNeuAn() {
|
||||||
for (JComponent feld : List.of(nameFeld, strasseFeld, plzFeld, ortFeld,
|
new KundenFormularDialog(SwingUtilities.getWindowAncestor(this), service, null)
|
||||||
eMailFeld, telefonFeld, ustIdNrFeld)) {
|
.setVisible(true);
|
||||||
((JTextField) feld).getDocument().addDocumentListener(aenderungsListener);
|
}
|
||||||
|
|
||||||
|
private void bearbeite() {
|
||||||
|
Kunde kunde = auswahl();
|
||||||
|
if (kunde == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
new KundenFormularDialog(SwingUtilities.getWindowAncestor(this), service, kunde)
|
||||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
.setVisible(true);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loesche() {
|
private void loesche() {
|
||||||
if (gewaehlteNummer == null) {
|
Kunde kunde = auswahl();
|
||||||
|
if (kunde == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int antwort = JOptionPane.showConfirmDialog(this,
|
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);
|
"Kunde löschen", JOptionPane.YES_NO_OPTION);
|
||||||
if (antwort != JOptionPane.YES_OPTION) {
|
if (antwort != JOptionPane.YES_OPTION) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||||
service.loescheKunde(gewaehlteNummer);
|
service.loescheKunde(kunde.getKundennummer());
|
||||||
leereFormular();
|
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gelöscht."), null);
|
||||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Der Kunde wurde gelöscht."), felder);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportiere() {
|
private void exportiere() {
|
||||||
JFileChooser auswahl = new JFileChooser();
|
JFileChooser auswahlDialog = new JFileChooser();
|
||||||
auswahl.setSelectedFile(new java.io.File("kunden.csv"));
|
auswahlDialog.setSelectedFile(new File("kunden.csv"));
|
||||||
if (auswahl.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
csvExport.exportiereCsv(auswahl.getSelectedFile().toPath());
|
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Die Kundenstammdaten wurden exportiert nach "
|
csvExport.exportiereCsv(auswahlDialog.getSelectedFile().toPath());
|
||||||
+ auswahl.getSelectedFile()), felder);
|
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
|
@Override
|
||||||
public boolean hatUngespeicherteAenderungen() {
|
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
|
@Override
|
||||||
public void aktualisiere() {
|
public void aktualisiere() {
|
||||||
String begriff = suchfeld.getText().trim();
|
List<Kunde> liste = controller.kundenListe(suchfeld.getText());
|
||||||
tabellenModel.setze(begriff.isEmpty()
|
tabellenModel.setze(liste);
|
||||||
? service.alleSortiertNachName()
|
trefferAnzeige.setText(UiHilfen.trefferText(liste.size(),
|
||||||
: service.suche(begriff));
|
controller.kundenListe("").size(), "Kunden", suchfeld.getText()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private DocumentListener neuerSuchListener() {
|
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 class KundenTabellenModel extends AbstractTableModel {
|
||||||
|
|
||||||
private static final String[] SPALTEN = {"Kundennummer", "Name", "Straße", "PLZ", "Ort"};
|
private static final String[] SPALTEN = {"Kundennummer", "Name", "Straße", "PLZ", "Ort"};
|
||||||
|
|
@ -297,6 +216,10 @@ public class KundenPanel extends JPanel implements ModulPanel {
|
||||||
fireTableDataChanged();
|
fireTableDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kunde gibZeile(int zeile) {
|
||||||
|
return kunden.get(zeile);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRowCount() {
|
public int getRowCount() {
|
||||||
return kunden.size();
|
return kunden.size();
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ package de.team1.faktura.gui;
|
||||||
*/
|
*/
|
||||||
public record Meldung(MeldungsTyp typ, String feldname, String text) {
|
public record Meldung(MeldungsTyp typ, String feldname, String text) {
|
||||||
|
|
||||||
|
/** Erfolgsmeldung ohne Feldbezug (D-F-17). */
|
||||||
public static Meldung erfolg(String text) {
|
public static Meldung erfolg(String text) {
|
||||||
return new Meldung(MeldungsTyp.ERFOLG, null, 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) {
|
public static Meldung fehler(String feldname, String text) {
|
||||||
return new Meldung(MeldungsTyp.FEHLER, feldname, text);
|
return new Meldung(MeldungsTyp.FEHLER, feldname, text);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,9 @@ public final class MeldungsAnzeige {
|
||||||
} catch (UncheckedIOException e) {
|
} catch (UncheckedIOException e) {
|
||||||
zeige(parent, Meldung.fehler(null,
|
zeige(parent, Meldung.fehler(null,
|
||||||
"Die Daten konnten nicht gespeichert werden: " + e.getMessage()), felder);
|
"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.DatenBereich;
|
||||||
import de.team1.faktura.gemeinsam.EreignisBus;
|
import de.team1.faktura.gemeinsam.EreignisBus;
|
||||||
import de.team1.faktura.gemeinsam.ValidierungsException;
|
|
||||||
import de.team1.faktura.produkte.Produkt;
|
import de.team1.faktura.produkte.Produkt;
|
||||||
import de.team1.faktura.produkte.ProduktCsvExport;
|
import de.team1.faktura.produkte.ProduktCsvExport;
|
||||||
import de.team1.faktura.produkte.ProduktVerwaltungsService;
|
import de.team1.faktura.produkte.ProduktVerwaltungsService;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComboBox;
|
|
||||||
import javax.swing.JComponent;
|
|
||||||
import javax.swing.JFileChooser;
|
import javax.swing.JFileChooser;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JSplitPane;
|
|
||||||
import javax.swing.JTable;
|
import javax.swing.JTable;
|
||||||
import javax.swing.JTextField;
|
import javax.swing.JTextField;
|
||||||
import javax.swing.ListSelectionModel;
|
import javax.swing.ListSelectionModel;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.event.DocumentEvent;
|
import javax.swing.event.DocumentEvent;
|
||||||
import javax.swing.event.DocumentListener;
|
import javax.swing.event.DocumentListener;
|
||||||
import javax.swing.table.AbstractTableModel;
|
import javax.swing.table.AbstractTableModel;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.FlowLayout;
|
import java.awt.FlowLayout;
|
||||||
import java.awt.GridBagConstraints;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.GridBagLayout;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.Insets;
|
import java.io.File;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modulansicht Produktverwaltung (D-F-03 bis F-05): sortierte Liste mit
|
* Modulansicht Produktverwaltung (D-F-03 bis F-05): sortierte Liste in voller
|
||||||
* Suchfeld, Formular mit gekennzeichneten Pflichtfeldern, CSV-Export.
|
* 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 {
|
public class ProduktPanel extends JPanel implements ModulPanel {
|
||||||
|
|
||||||
private static final String[] STEUERSAETZE = {"19 %", "7 %", "0 %"};
|
|
||||||
|
|
||||||
private final ProduktVerwaltungsService service;
|
private final ProduktVerwaltungsService service;
|
||||||
|
private final StammdatenController controller;
|
||||||
private final ProduktCsvExport csvExport;
|
private final ProduktCsvExport csvExport;
|
||||||
|
|
||||||
private final JTextField suchfeld = new JTextField(20);
|
private final JTextField suchfeld = new JTextField(20);
|
||||||
private final ProduktTabellenModel tabellenModel = new ProduktTabellenModel();
|
private final ProduktTabellenModel tabellenModel = new ProduktTabellenModel();
|
||||||
private final JTable tabelle = new JTable(tabellenModel);
|
private final JTable tabelle = new JTable(tabellenModel);
|
||||||
|
private final JLabel trefferAnzeige = UiHilfen.trefferLabel();
|
||||||
|
|
||||||
private final JTextField bezeichnungFeld = new JTextField(20);
|
private final JButton bearbeitenKnopf = new JButton("Bearbeiten…");
|
||||||
private final JTextField beschreibungFeld = new JTextField(20);
|
private final JButton loeschenKnopf = new JButton("Löschen");
|
||||||
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 String gewaehlteNummer;
|
/**
|
||||||
private boolean ungespeichert;
|
* @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)
|
||||||
public ProduktPanel(ProduktVerwaltungsService service, ProduktCsvExport csvExport,
|
* @param csvExport CSV-Export der Produktstammdaten (B-F-13)
|
||||||
EreignisBus ereignisBus) {
|
* @param ereignisBus Observer-Verteiler für Aktualisierungen nach Datenänderungen
|
||||||
|
*/
|
||||||
|
public ProduktPanel(ProduktVerwaltungsService service, StammdatenController controller,
|
||||||
|
ProduktCsvExport csvExport, EreignisBus ereignisBus) {
|
||||||
this.service = service;
|
this.service = service;
|
||||||
|
this.controller = controller;
|
||||||
this.csvExport = csvExport;
|
this.csvExport = csvExport;
|
||||||
felder.put("Bezeichnung", bezeichnungFeld);
|
|
||||||
felder.put("Einzelpreis", preisFeld);
|
|
||||||
felder.put("Steuersatz", steuersatzWahl);
|
|
||||||
baueOberflaeche();
|
baueOberflaeche();
|
||||||
aktualisiere();
|
aktualisiere();
|
||||||
ereignisBus.abonniere(DatenBereich.PRODUKTE, this::aktualisiere);
|
ereignisBus.abonniere(DatenBereich.PRODUKTE, this::aktualisiere);
|
||||||
|
|
@ -73,9 +67,12 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
||||||
|
|
||||||
private void baueOberflaeche() {
|
private void baueOberflaeche() {
|
||||||
setLayout(new BorderLayout(8, 8));
|
setLayout(new BorderLayout(8, 8));
|
||||||
|
setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
|
||||||
|
|
||||||
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
JPanel suchleiste = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
|
||||||
suchleiste.add(new JLabel("Suche (Bezeichnung oder Produktnummer):"));
|
suchleiste.add(new JLabel("Suche:"));
|
||||||
|
UiHilfen.platzhalter(suchfeld, "Bezeichnung oder Produktnummer…");
|
||||||
|
suchfeld.setToolTipText("Suche nach Bezeichnung oder Produktnummer (Strg+F)");
|
||||||
suchleiste.add(suchfeld);
|
suchleiste.add(suchfeld);
|
||||||
suchfeld.getDocument().addDocumentListener(neuerSuchListener());
|
suchfeld.getDocument().addDocumentListener(neuerSuchListener());
|
||||||
add(suchleiste, BorderLayout.NORTH);
|
add(suchleiste, BorderLayout.NORTH);
|
||||||
|
|
@ -83,191 +80,112 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
||||||
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
tabelle.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
TabellenFormat.konfiguriere(tabelle);
|
TabellenFormat.konfiguriere(tabelle);
|
||||||
tabelle.getColumnModel().getColumn(2).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
tabelle.getColumnModel().getColumn(2).setCellRenderer(TabellenFormat.waehrungsRenderer());
|
||||||
tabelle.getSelectionModel().addListSelectionListener(e -> {
|
tabelle.getSelectionModel().addListSelectionListener(e -> aktualisiereAktionen());
|
||||||
if (!e.getValueIsAdjusting()) {
|
tabelle.addMouseListener(new MouseAdapter() {
|
||||||
ladeAuswahl();
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.getClickCount() == 2) {
|
||||||
|
bearbeite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
JSplitPane teiler = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
|
JPanel listenSeite = new JPanel(new BorderLayout(0, 4));
|
||||||
new JScrollPane(tabelle), baueFormular());
|
listenSeite.add(new JScrollPane(tabelle), BorderLayout.CENTER);
|
||||||
teiler.setResizeWeight(0.55);
|
listenSeite.add(trefferAnzeige, BorderLayout.SOUTH);
|
||||||
add(teiler, BorderLayout.CENTER);
|
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() {
|
private Produkt auswahl() {
|
||||||
JPanel formular = new JPanel(new GridBagLayout());
|
int zeile = tabelle.getSelectedRow();
|
||||||
GridBagConstraints c = new GridBagConstraints();
|
return zeile < 0 ? null
|
||||||
c.insets = new Insets(3, 3, 3, 3);
|
: tabellenModel.gibZeile(tabelle.convertRowIndexToModel(zeile));
|
||||||
c.anchor = GridBagConstraints.WEST;
|
}
|
||||||
c.fill = GridBagConstraints.HORIZONTAL;
|
|
||||||
|
|
||||||
int zeile = 0;
|
private void aktualisiereAktionen() {
|
||||||
zeile = formularZeile(formular, c, zeile, "Produktnummer:", nummerAnzeige);
|
boolean ausgewaehlt = auswahl() != null;
|
||||||
zeile = formularZeile(formular, c, zeile, "Bezeichnung: *", bezeichnungFeld);
|
String hinweis = "Bitte zuerst ein Produkt in der Liste auswählen.";
|
||||||
zeile = formularZeile(formular, c, zeile, "Beschreibung:", beschreibungFeld);
|
UiHilfen.schalteAktion(bearbeitenKnopf, ausgewaehlt, hinweis);
|
||||||
zeile = formularZeile(formular, c, zeile, "Einzelpreis (netto): *", preisFeld);
|
UiHilfen.schalteAktion(loeschenKnopf, ausgewaehlt, hinweis);
|
||||||
zeile = formularZeile(formular, c, zeile, "Steuersatz: *", steuersatzWahl);
|
}
|
||||||
zeile = formularZeile(formular, c, zeile, "Einheit:", einheitFeld);
|
|
||||||
|
|
||||||
DocumentListener aenderungsListener = neuerAenderungsListener();
|
private void legeNeuAn() {
|
||||||
for (JTextField feld : List.of(bezeichnungFeld, beschreibungFeld, preisFeld, einheitFeld)) {
|
new ProduktFormularDialog(SwingUtilities.getWindowAncestor(this), service, null)
|
||||||
feld.getDocument().addDocumentListener(aenderungsListener);
|
.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bearbeite() {
|
||||||
|
Produkt produkt = auswahl();
|
||||||
|
if (produkt == null) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
new ProduktFormularDialog(SwingUtilities.getWindowAncestor(this), service, produkt)
|
||||||
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
.setVisible(true);
|
||||||
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");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loesche() {
|
private void loesche() {
|
||||||
if (gewaehlteNummer == null) {
|
Produkt produkt = auswahl();
|
||||||
|
if (produkt == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int antwort = JOptionPane.showConfirmDialog(this,
|
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);
|
"Produkt löschen", JOptionPane.YES_NO_OPTION);
|
||||||
if (antwort != JOptionPane.YES_OPTION) {
|
if (antwort != JOptionPane.YES_OPTION) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MeldungsAnzeige.mitFehlerbehandlung(this, felder, () -> {
|
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||||
service.loescheProdukt(gewaehlteNummer);
|
service.loescheProdukt(produkt.getProduktnummer());
|
||||||
leereFormular();
|
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gelöscht."), null);
|
||||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Das Produkt wurde gelöscht."), felder);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportiere() {
|
private void exportiere() {
|
||||||
JFileChooser auswahl = new JFileChooser();
|
JFileChooser auswahlDialog = new JFileChooser();
|
||||||
auswahl.setSelectedFile(new java.io.File("produkte.csv"));
|
auswahlDialog.setSelectedFile(new File("produkte.csv"));
|
||||||
if (auswahl.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
if (auswahlDialog.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
|
||||||
csvExport.exportiereCsv(auswahl.getSelectedFile().toPath());
|
MeldungsAnzeige.mitFehlerbehandlung(this, null, () -> {
|
||||||
MeldungsAnzeige.zeige(this, Meldung.erfolg("Die Produktstammdaten wurden exportiert nach "
|
csvExport.exportiereCsv(auswahlDialog.getSelectedFile().toPath());
|
||||||
+ auswahl.getSelectedFile()), felder);
|
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
|
@Override
|
||||||
public boolean hatUngespeicherteAenderungen() {
|
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
|
@Override
|
||||||
public void aktualisiere() {
|
public void aktualisiere() {
|
||||||
String begriff = suchfeld.getText().trim();
|
List<Produkt> liste = controller.produkteListe(suchfeld.getText());
|
||||||
tabellenModel.setze(begriff.isEmpty()
|
tabellenModel.setze(liste);
|
||||||
? service.alleSortiertNachBezeichnung()
|
trefferAnzeige.setText(UiHilfen.trefferText(liste.size(),
|
||||||
: service.suche(begriff));
|
controller.produkteListe("").size(), "Produkte", suchfeld.getText()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private DocumentListener neuerSuchListener() {
|
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 class ProduktTabellenModel extends AbstractTableModel {
|
||||||
|
|
||||||
private static final String[] SPALTEN = {"Produktnummer", "Bezeichnung", "Einzelpreis (netto)", "Steuersatz", "Einheit"};
|
private static final String[] SPALTEN = {"Produktnummer", "Bezeichnung", "Einzelpreis (netto)", "Steuersatz", "Einheit"};
|
||||||
|
|
@ -319,6 +218,10 @@ public class ProduktPanel extends JPanel implements ModulPanel {
|
||||||
fireTableDataChanged();
|
fireTableDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Produkt gibZeile(int zeile) {
|
||||||
|
return produkte.get(zeile);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRowCount() {
|
public int getRowCount() {
|
||||||
return produkte.size();
|
return produkte.size();
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,16 @@ import de.team1.faktura.kunden.KundenService;
|
||||||
import de.team1.faktura.produkte.Produkt;
|
import de.team1.faktura.produkte.Produkt;
|
||||||
import de.team1.faktura.produkte.ProduktService;
|
import de.team1.faktura.produkte.ProduktService;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
import javax.swing.DefaultComboBoxModel;
|
import javax.swing.DefaultComboBoxModel;
|
||||||
import javax.swing.DefaultListModel;
|
import javax.swing.DefaultListModel;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JComponent;
|
||||||
import javax.swing.JDialog;
|
import javax.swing.JDialog;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
import javax.swing.JList;
|
import javax.swing.JList;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JSpinner;
|
import javax.swing.JSpinner;
|
||||||
|
|
@ -19,11 +22,13 @@ import javax.swing.JTextArea;
|
||||||
import javax.swing.JTextField;
|
import javax.swing.JTextField;
|
||||||
import javax.swing.ListSelectionModel;
|
import javax.swing.ListSelectionModel;
|
||||||
import javax.swing.SpinnerNumberModel;
|
import javax.swing.SpinnerNumberModel;
|
||||||
|
import javax.swing.UIManager;
|
||||||
import javax.swing.event.DocumentEvent;
|
import javax.swing.event.DocumentEvent;
|
||||||
import javax.swing.event.DocumentListener;
|
import javax.swing.event.DocumentListener;
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
import java.awt.CardLayout;
|
import java.awt.CardLayout;
|
||||||
import java.awt.FlowLayout;
|
import java.awt.FlowLayout;
|
||||||
|
import java.awt.Font;
|
||||||
import java.awt.Window;
|
import java.awt.Window;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
@ -63,6 +68,7 @@ public class RechnungsWizardDialog extends JDialog {
|
||||||
private final JButton weiterKnopf = new JButton("Weiter >");
|
private final JButton weiterKnopf = new JButton("Weiter >");
|
||||||
private final JButton speichernKnopf = new JButton("Speichern");
|
private final JButton speichernKnopf = new JButton("Speichern");
|
||||||
private final JLabel schrittAnzeige = new JLabel();
|
private final JLabel schrittAnzeige = new JLabel();
|
||||||
|
private final JLabel[] schrittMarkierungen = new JLabel[WizardSchritt.values().length];
|
||||||
|
|
||||||
public RechnungsWizardDialog(Window besitzer, RechnungsWizardController controller,
|
public RechnungsWizardDialog(Window besitzer, RechnungsWizardController controller,
|
||||||
KundenService kundenService, ProduktService produktService) {
|
KundenService kundenService, ProduktService produktService) {
|
||||||
|
|
@ -80,7 +86,22 @@ public class RechnungsWizardDialog extends JDialog {
|
||||||
|
|
||||||
private void baueOberflaeche() {
|
private void baueOberflaeche() {
|
||||||
setLayout(new BorderLayout(8, 8));
|
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(baueSchrittKunde(), WizardSchritt.KUNDE_WAEHLEN.name());
|
||||||
kartenPanel.add(baueSchrittPositionen(), WizardSchritt.POSITIONEN_ERFASSEN.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));
|
JPanel knoepfe = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
JButton abbrechen = new JButton("Abbrechen");
|
JButton abbrechen = new JButton("Abbrechen");
|
||||||
abbrechen.addActionListener(e -> dispose());
|
abbrechen.setMnemonic('A');
|
||||||
|
abbrechen.addActionListener(e -> abbrechenMitNachfrage());
|
||||||
|
zurueckKnopf.setMnemonic('Z');
|
||||||
zurueckKnopf.addActionListener(e -> {
|
zurueckKnopf.addActionListener(e -> {
|
||||||
controller.zurueck();
|
controller.zurueck();
|
||||||
zeigeSchritt();
|
zeigeSchritt();
|
||||||
});
|
});
|
||||||
|
weiterKnopf.setMnemonic('W');
|
||||||
weiterKnopf.addActionListener(e -> weiter());
|
weiterKnopf.addActionListener(e -> weiter());
|
||||||
|
speichernKnopf.setMnemonic('S');
|
||||||
speichernKnopf.addActionListener(e -> speichere());
|
speichernKnopf.addActionListener(e -> speichere());
|
||||||
knoepfe.add(abbrechen);
|
knoepfe.add(abbrechen);
|
||||||
knoepfe.add(zurueckKnopf);
|
knoepfe.add(zurueckKnopf);
|
||||||
knoepfe.add(weiterKnopf);
|
knoepfe.add(weiterKnopf);
|
||||||
knoepfe.add(speichernKnopf);
|
knoepfe.add(speichernKnopf);
|
||||||
add(knoepfe, BorderLayout.SOUTH);
|
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). */
|
/** Schritt 1: Kunde auswählen (F-09). */
|
||||||
|
|
@ -147,6 +191,7 @@ public class RechnungsWizardDialog extends JDialog {
|
||||||
eingabe.add(new JLabel("Menge:"));
|
eingabe.add(new JLabel("Menge:"));
|
||||||
eingabe.add(mengeWahl);
|
eingabe.add(mengeWahl);
|
||||||
JButton hinzufuegen = new JButton("Hinzufügen");
|
JButton hinzufuegen = new JButton("Hinzufügen");
|
||||||
|
hinzufuegen.setMnemonic('H');
|
||||||
hinzufuegen.addActionListener(e -> {
|
hinzufuegen.addActionListener(e -> {
|
||||||
Produkt produkt = (Produkt) produktWahl.getSelectedItem();
|
Produkt produkt = (Produkt) produktWahl.getSelectedItem();
|
||||||
if (produkt == null) {
|
if (produkt == null) {
|
||||||
|
|
@ -160,6 +205,7 @@ public class RechnungsWizardDialog extends JDialog {
|
||||||
});
|
});
|
||||||
eingabe.add(hinzufuegen);
|
eingabe.add(hinzufuegen);
|
||||||
JButton entfernen = new JButton("Entfernen");
|
JButton entfernen = new JButton("Entfernen");
|
||||||
|
entfernen.setMnemonic('E');
|
||||||
entfernen.addActionListener(e -> {
|
entfernen.addActionListener(e -> {
|
||||||
int index = positionsListe.getSelectedIndex();
|
int index = positionsListe.getSelectedIndex();
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
|
@ -178,9 +224,12 @@ public class RechnungsWizardDialog extends JDialog {
|
||||||
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
panel.add(new JLabel("Rechnungsdatum (TT.MM.JJJJ): *"));
|
panel.add(new JLabel("Rechnungsdatum (TT.MM.JJJJ): *"));
|
||||||
rechnungsdatumFeld.setText(DATUM.format(LocalDate.now()));
|
rechnungsdatumFeld.setText(DATUM.format(LocalDate.now()));
|
||||||
|
rechnungsdatumFeld.setToolTipText("Pflichtfeld — Format: TT.MM.JJJJ");
|
||||||
panel.add(rechnungsdatumFeld);
|
panel.add(rechnungsdatumFeld);
|
||||||
panel.add(new JLabel("Zahlungsziel (leer = 14 Tage):"));
|
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(zahlungszielFeld);
|
||||||
|
panel.add(UiHilfen.pflichtfeldLegende());
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,10 +300,20 @@ public class RechnungsWizardDialog extends JDialog {
|
||||||
zusammenfassung.setText(controller.erzeugeZusammenfassung());
|
zusammenfassung.setText(controller.erzeugeZusammenfassung());
|
||||||
}
|
}
|
||||||
karten.show(kartenPanel, schritt.name());
|
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);
|
zurueckKnopf.setEnabled(schritt.ordinal() > 0);
|
||||||
weiterKnopf.setEnabled(schritt != WizardSchritt.SPEICHERN);
|
weiterKnopf.setEnabled(schritt != WizardSchritt.SPEICHERN);
|
||||||
speichernKnopf.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) {
|
private static String schrittName(WizardSchritt schritt) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import java.util.List;
|
||||||
/**
|
/**
|
||||||
* Controller der Stammdaten-Ansichten (D-F-03): delegiert Suchanfragen an
|
* Controller der Stammdaten-Ansichten (D-F-03): delegiert Suchanfragen an
|
||||||
* die Dienste der Gruppen B und C; die GUI rechnet und filtert selbst nicht.
|
* 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 {
|
public class StammdatenController {
|
||||||
|
|
||||||
|
|
@ -21,11 +22,31 @@ public class StammdatenController {
|
||||||
this.produktService = produktService;
|
this.produktService = produktService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Volltextsuche über Name oder Kundennummer (C-F-12). */
|
||||||
public List<Kunde> sucheKunden(String suchbegriff) {
|
public List<Kunde> sucheKunden(String suchbegriff) {
|
||||||
return kundenService.suche(suchbegriff);
|
return kundenService.suche(suchbegriff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Volltextsuche über Bezeichnung oder Produktnummer (B-F-12). */
|
||||||
public List<Produkt> sucheProdukte(String suchbegriff) {
|
public List<Produkt> sucheProdukte(String suchbegriff) {
|
||||||
return produktService.suche(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. */
|
/** Einheitliche Grundeinstellungen: Zeilenhöhe und Sortierung per Spaltenkopf. */
|
||||||
public static void konfiguriere(JTable tabelle) {
|
public static void konfiguriere(JTable tabelle) {
|
||||||
tabelle.setRowHeight(24);
|
tabelle.setRowHeight(26);
|
||||||
tabelle.setAutoCreateRowSorter(true);
|
tabelle.setAutoCreateRowSorter(true);
|
||||||
tabelle.setFillsViewportHeight(true);
|
tabelle.setFillsViewportHeight(true);
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +66,8 @@ public final class TabellenFormat {
|
||||||
|
|
||||||
private static Color farbeFuer(DokumentStatus status) {
|
private static Color farbeFuer(DokumentStatus status) {
|
||||||
return switch (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 OFFEN -> new Color(0, 90, 180);
|
||||||
case VERSENDET -> new Color(0, 130, 60);
|
case VERSENDET -> new Color(0, 130, 60);
|
||||||
case STORNIERT -> new Color(180, 40, 40);
|
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());
|
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. */
|
/** Zähl-Stub des DokumentService (Gruppe A) für die Controller-Tests. */
|
||||||
private static final class DokumentServiceStub implements DokumentService {
|
private static final class DokumentServiceStub implements DokumentService {
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue