Erweiterungen
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -82,6 +82,12 @@
|
||||
<version>1.18.38</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.librepdf</groupId>
|
||||
<artifactId>openpdf</artifactId>
|
||||
<version>1.3.30</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
19
src/main/java/de/assecutor/votianlt/model/Invoice.java
Normal file
19
src/main/java/de/assecutor/votianlt/model/Invoice.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package de.assecutor.votianlt.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Invoice {
|
||||
private String id;
|
||||
private String kunde;
|
||||
private LocalDate datum;
|
||||
private double betrag;
|
||||
private String beschreibung;
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@ public final class MainLayout extends AppLayout {
|
||||
SideNavItem customers = new SideNavItem("Kunden", "customers", new Icon(VaadinIcon.USERS));
|
||||
SideNavItem appUsers = new SideNavItem("App-Nutzer", "app-user", new Icon(VaadinIcon.USERS));
|
||||
SideNavItem devices = new SideNavItem("Endgeräte", "app-devices", new Icon(VaadinIcon.MOBILE));
|
||||
SideNavItem invoices = new SideNavItem("Rechnungen", "5", new Icon(VaadinIcon.COG));
|
||||
SideNavItem statistics = new SideNavItem("Statistik", "6", new Icon(VaadinIcon.COG));
|
||||
SideNavItem invoices = new SideNavItem("Rechnungen", "invoices", new Icon(VaadinIcon.FILE_TEXT));
|
||||
SideNavItem statistics = new SideNavItem("Statistiken", "statistics", new Icon(VaadinIcon.BAR_CHART));
|
||||
|
||||
verwaltungContent.add(jobs, customers, appUsers, devices, invoices, statistics);
|
||||
verwaltungDetails.add(verwaltungContent);
|
||||
|
||||
104
src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java
Normal file
104
src/main/java/de/assecutor/votianlt/pages/view/InvoicesView.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import de.assecutor.votianlt.model.Invoice;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import com.vaadin.flow.server.StreamRegistration;
|
||||
|
||||
@PageTitle("Rechnungen")
|
||||
@Route(value = "invoices", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||
@RolesAllowed({"USER","ADMIN"})
|
||||
public class InvoicesView extends VerticalLayout {
|
||||
|
||||
private final Grid<Invoice> invoiceGrid;
|
||||
|
||||
public InvoicesView() {
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER);
|
||||
setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
|
||||
H2 title = new H2("Rechnungen");
|
||||
add(title);
|
||||
|
||||
invoiceGrid = new Grid<>(Invoice.class, false);
|
||||
invoiceGrid.addColumn(Invoice::getId).setHeader("Rechnungsnummer").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(Invoice::getKunde).setHeader("Kunde").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(Invoice::getDatum).setHeader("Datum").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(Invoice::getBetrag).setHeader("Betrag").setAutoWidth(true);
|
||||
invoiceGrid.addColumn(Invoice::getBeschreibung).setHeader("Beschreibung").setAutoWidth(true);
|
||||
invoiceGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
|
||||
invoiceGrid.getStyle().set("cursor", "pointer");
|
||||
|
||||
// Testdaten
|
||||
List<Invoice> testInvoices = List.of(
|
||||
new Invoice("R-2024-001", "Max Mustermann", LocalDate.now().minusDays(2), 199.99, "Transport Hamburg-Berlin"),
|
||||
new Invoice("R-2024-002", "Erika Musterfrau", LocalDate.now().minusDays(1), 299.49, "Express München-Köln"),
|
||||
new Invoice("R-2024-003", "Hans Beispiel", LocalDate.now(), 149.00, "Standard Leipzig-Dresden")
|
||||
);
|
||||
invoiceGrid.setItems(testInvoices);
|
||||
|
||||
invoiceGrid.addItemClickListener(event -> {
|
||||
Invoice invoice = event.getItem();
|
||||
if (invoice != null) {
|
||||
downloadInvoicePdf(invoice);
|
||||
}
|
||||
});
|
||||
|
||||
add(invoiceGrid);
|
||||
}
|
||||
|
||||
private void downloadInvoicePdf(Invoice invoice) {
|
||||
try {
|
||||
// PDF generieren
|
||||
byte[] pdfBytes = generatePdf(invoice);
|
||||
StreamResource resource = new StreamResource(
|
||||
invoice.getId() + ".pdf",
|
||||
() -> new ByteArrayInputStream(pdfBytes)
|
||||
);
|
||||
resource.setContentType("application/pdf");
|
||||
resource.setCacheTime(0);
|
||||
|
||||
// Direkter Download über UI
|
||||
StreamRegistration registration = UI.getCurrent().getSession().getResourceRegistry().registerResource(resource);
|
||||
UI.getCurrent().getPage().open(registration.getResourceUri().toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
Notification.show("Fehler beim Erstellen der PDF: " + e.getMessage(), 5000, Notification.Position.MIDDLE);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generatePdf(Invoice invoice) {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
// Einfache PDF mit iText (oder OpenPDF, falls iText nicht verfügbar)
|
||||
com.lowagie.text.Document document = new com.lowagie.text.Document();
|
||||
com.lowagie.text.pdf.PdfWriter.getInstance(document, out);
|
||||
document.open();
|
||||
document.add(new com.lowagie.text.Paragraph("Rechnung: " + invoice.getId()));
|
||||
document.add(new com.lowagie.text.Paragraph("Kunde: " + invoice.getKunde()));
|
||||
document.add(new com.lowagie.text.Paragraph("Datum: " + invoice.getDatum()));
|
||||
document.add(new com.lowagie.text.Paragraph("Betrag: " + invoice.getBetrag() + " EUR"));
|
||||
document.add(new com.lowagie.text.Paragraph("Beschreibung: " + invoice.getBeschreibung()));
|
||||
document.close();
|
||||
return out.toByteArray();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("PDF-Generierung fehlgeschlagen: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
package de.assecutor.votianlt.pages.view;
|
||||
|
||||
import com.vaadin.flow.component.Html;
|
||||
import com.vaadin.flow.component.dependency.JavaScript;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.H3;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
@PageTitle("Statistiken")
|
||||
@Route(value = "statistics", layout = de.assecutor.votianlt.pages.base.ui.view.MainLayout.class)
|
||||
@RolesAllowed({"USER","ADMIN"})
|
||||
@JavaScript("https://cdn.jsdelivr.net/npm/chart.js")
|
||||
public class StatisticsView extends VerticalLayout {
|
||||
|
||||
public StatisticsView() {
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
H2 title = new H2("Statistiken");
|
||||
add(title);
|
||||
|
||||
// KPI Cards
|
||||
HorizontalLayout kpiLayout = createKpiCards();
|
||||
add(kpiLayout);
|
||||
|
||||
// Charts Layout
|
||||
HorizontalLayout chartsLayout = new HorizontalLayout();
|
||||
chartsLayout.setWidthFull();
|
||||
chartsLayout.setHeight("400px");
|
||||
chartsLayout.setSpacing(true);
|
||||
|
||||
// Aufträge pro Monat (Liniendiagramm)
|
||||
Div monthlyOrdersChart = createMonthlyOrdersChart();
|
||||
monthlyOrdersChart.setWidth("50%");
|
||||
monthlyOrdersChart.setHeight("100%");
|
||||
chartsLayout.add(monthlyOrdersChart);
|
||||
|
||||
// Aufträge nach Status (Kreisdiagramm)
|
||||
Div statusChart = createStatusPieChart();
|
||||
statusChart.setWidth("50%");
|
||||
statusChart.setHeight("100%");
|
||||
chartsLayout.add(statusChart);
|
||||
|
||||
add(chartsLayout);
|
||||
|
||||
// Umsatz nach Kunden (Balkendiagramm)
|
||||
VerticalLayout revenueContainer = new VerticalLayout();
|
||||
revenueContainer.setWidthFull();
|
||||
revenueContainer.setHeight("400px");
|
||||
revenueContainer.setPadding(false);
|
||||
|
||||
Div revenueChart = createRevenueByCustomerChart();
|
||||
revenueChart.setSizeFull();
|
||||
revenueContainer.add(revenueChart);
|
||||
|
||||
add(revenueContainer);
|
||||
}
|
||||
|
||||
private HorizontalLayout createKpiCards() {
|
||||
HorizontalLayout kpiLayout = new HorizontalLayout();
|
||||
kpiLayout.setWidthFull();
|
||||
kpiLayout.setSpacing(true);
|
||||
kpiLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.EVENLY);
|
||||
|
||||
// Gesamtaufträge
|
||||
Div totalOrdersCard = createKpiCard("Gesamtaufträge", "247", "success");
|
||||
|
||||
// Offene Aufträge
|
||||
Div openOrdersCard = createKpiCard("Offene Aufträge", "34", "warning");
|
||||
|
||||
// Umsatz diesen Monat
|
||||
Div revenueCard = createKpiCard("Umsatz (Monat)", "€ 24.500", "primary");
|
||||
|
||||
// Neue Kunden
|
||||
Div newCustomersCard = createKpiCard("Neue Kunden", "12", "success");
|
||||
|
||||
kpiLayout.add(totalOrdersCard, openOrdersCard, revenueCard, newCustomersCard);
|
||||
return kpiLayout;
|
||||
}
|
||||
|
||||
private Div createKpiCard(String title, String value, String theme) {
|
||||
Div card = new Div();
|
||||
card.addClassName("kpi-card");
|
||||
card.getStyle()
|
||||
.set("background", "var(--lumo-base-color)")
|
||||
.set("border", "1px solid var(--lumo-contrast-10pct)")
|
||||
.set("border-radius", "var(--lumo-border-radius-m)")
|
||||
.set("padding", "var(--lumo-space-m)")
|
||||
.set("text-align", "center")
|
||||
.set("box-shadow", "var(--lumo-box-shadow-xs)")
|
||||
.set("min-width", "150px");
|
||||
|
||||
H3 titleElement = new H3(title);
|
||||
titleElement.getStyle().set("margin", "0 0 var(--lumo-space-s) 0").set("font-size", "var(--lumo-font-size-s)");
|
||||
|
||||
Span valueElement = new Span(value);
|
||||
valueElement.getStyle()
|
||||
.set("font-size", "var(--lumo-font-size-xl)")
|
||||
.set("font-weight", "bold")
|
||||
.set("color", getThemeColor(theme));
|
||||
|
||||
card.add(titleElement, valueElement);
|
||||
return card;
|
||||
}
|
||||
|
||||
private String getThemeColor(String theme) {
|
||||
return switch (theme) {
|
||||
case "success" -> "var(--lumo-success-color)";
|
||||
case "warning" -> "var(--lumo-warning-color)";
|
||||
case "error" -> "var(--lumo-error-color)";
|
||||
default -> "var(--lumo-primary-color)";
|
||||
};
|
||||
}
|
||||
|
||||
private Div createMonthlyOrdersChart() {
|
||||
Div chartContainer = new Div();
|
||||
chartContainer.setId("monthlyOrdersChart");
|
||||
|
||||
String canvasHtml = "<canvas id='monthlyOrdersCanvas' style='width: 100%; height: 100%;'></canvas>";
|
||||
Html canvas = new Html(canvasHtml);
|
||||
chartContainer.add(canvas);
|
||||
|
||||
String script = """
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('monthlyOrdersCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
|
||||
datasets: [{
|
||||
label: '2024',
|
||||
data: [15, 18, 22, 28, 32, 35, 42, 38, 41, 35, 28, 25],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1
|
||||
}, {
|
||||
label: '2023',
|
||||
data: [12, 15, 18, 25, 28, 30, 35, 32, 36, 30, 25, 22],
|
||||
borderColor: 'rgb(135, 206, 235)',
|
||||
backgroundColor: 'rgba(135, 206, 235, 0.2)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Aufträge pro Monat'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Anzahl Aufträge'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
|
||||
Html scriptElement = new Html(script);
|
||||
chartContainer.add(scriptElement);
|
||||
|
||||
return chartContainer;
|
||||
}
|
||||
|
||||
private Div createStatusPieChart() {
|
||||
Div chartContainer = new Div();
|
||||
chartContainer.setId("statusPieChart");
|
||||
|
||||
String canvasHtml = "<canvas id='statusPieCanvas' style='width: 100%; height: 100%;'></canvas>";
|
||||
Html canvas = new Html(canvasHtml);
|
||||
chartContainer.add(canvas);
|
||||
|
||||
String script = """
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('statusPieCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Abgeschlossen', 'In Bearbeitung', 'Geplant', 'Storniert'],
|
||||
datasets: [{
|
||||
data: [156, 34, 28, 12],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.8)',
|
||||
'rgba(255, 206, 86, 0.8)',
|
||||
'rgba(75, 192, 192, 0.8)',
|
||||
'rgba(255, 99, 132, 0.8)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(255, 99, 132, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Aufträge nach Status'
|
||||
},
|
||||
legend: {
|
||||
position: 'right'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
|
||||
Html scriptElement = new Html(script);
|
||||
chartContainer.add(scriptElement);
|
||||
|
||||
return chartContainer;
|
||||
}
|
||||
|
||||
private Div createRevenueByCustomerChart() {
|
||||
Div chartContainer = new Div();
|
||||
chartContainer.setId("revenueByCustomerChart");
|
||||
|
||||
String canvasHtml = "<canvas id='revenueByCustomerCanvas' style='width: 100%; height: 100%;'></canvas>";
|
||||
Html canvas = new Html(canvasHtml);
|
||||
chartContainer.add(canvas);
|
||||
|
||||
String script = """
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
const ctx = document.getElementById('revenueByCustomerCanvas');
|
||||
if (ctx) {
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Firma A GmbH', 'Logistics B', 'Transport C', 'Spediteur D', 'Handel E',
|
||||
'Industrie F', 'Service G', 'Vertrieb H', 'Export I', 'Import J'],
|
||||
datasets: [{
|
||||
label: 'Umsatz (€)',
|
||||
data: [8500, 7200, 6800, 5900, 5400, 4800, 4200, 3900, 3500, 3100],
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.8)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Top 10 Kunden nach Umsatz'
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: {
|
||||
maxRotation: 45,
|
||||
minRotation: 45
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Umsatz (€)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
</script>
|
||||
""";
|
||||
|
||||
Html scriptElement = new Html(script);
|
||||
chartContainer.add(scriptElement);
|
||||
|
||||
return chartContainer;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user