Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38d3cbe38a | |||
| 9114ea9800 | |||
| 9316b83f82 |
@@ -16,7 +16,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: '25'
|
java-version: '25'
|
||||||
cache: maven
|
|
||||||
|
|
||||||
- name: Build and Test
|
- name: Build and Test
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
key de en es status
|
||||||
|
@@ -0,0 +1,39 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mvnw spring-boot:run # run app (default goal) — http://localhost:8080, ~30s first start
|
||||||
|
./mvnw compile # compile only
|
||||||
|
./mvnw package # production build → target/*.jar
|
||||||
|
java -jar target/*.jar # run production jar
|
||||||
|
```
|
||||||
|
|
||||||
|
No test sources exist yet (`src/test` is empty) — `spring-boot-starter-test` and `browserless-test-spring` are on the classpath but unused.
|
||||||
|
|
||||||
|
**Java 25 toolchain required** (`pom.xml` sets `java.version=25`). If the default JDK on PATH is older (check `java -version`), point `JAVA_HOME` at a Java 25 install for the Maven build, e.g.:
|
||||||
|
```bash
|
||||||
|
JAVA_HOME="/path/to/jdk-25" ./mvnw compile
|
||||||
|
```
|
||||||
|
|
||||||
|
Port 8080 conflicts: a prior `spring-boot:run` left running in the background is the usual cause (Vaadin dev mode keeps a second process/thread alive under a different PID than the launching Maven process — killing the Maven process alone may not free the port). Find and stop the actual listener before restarting.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Vaadin Flow (server-side Java UI, no hand-written HTML/JS for views) on Spring Boot 4.1, using the **Aura** theme (not Lumo — no `@Theme` annotation; Aura is wired via `@StyleSheet(Aura.STYLESHEET)` in `Application.java`).
|
||||||
|
|
||||||
|
**Charts are ApexCharts, not Vaadin Charts.** The bridge lives in `components/`:
|
||||||
|
- `ApexChart` (abstract, `@Tag("apex-chart")`) — owns the JS module (`frontend/components/apex-chart.ts`, a Lit element rendering into light DOM), serializes an options `Map` to JSON via Jackson and calls `renderChart` client-side, and exposes point-click events back to the server via `@ClientCallable`.
|
||||||
|
- `AxisChart` (abstract) — builds `series`/`xaxis.categories` options for line/bar.
|
||||||
|
- `LineChart`, `BarChart` extend `AxisChart`; `PieChart` extends `ApexChart` directly (`series`/`labels` instead of axis-based).
|
||||||
|
- `DialectTheme` — single source of chart styling (categorical color palette, grid/legend/stroke option fragments). Every chart's `setData(...)` starts from `DialectTheme.baseOptions(chartType)` and merges in its data. Add new chart types here, not by duplicating option maps.
|
||||||
|
|
||||||
|
Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this`), global CSS can reach into the chart markup — but series/legend/grid colors are driven entirely by the options JSON, not CSS, because ApexCharts renders its own SVG/canvas.
|
||||||
|
|
||||||
|
**Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) wraps each chart in a `Card` (`components/Card.java`); `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`.
|
||||||
|
|
||||||
|
**Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components.
|
||||||
|
|
||||||
|
UI copy/data (chart labels, notifications) is in German.
|
||||||
@@ -6,38 +6,42 @@ import com.example.components.LineChart;
|
|||||||
import com.example.components.PieChart;
|
import com.example.components.PieChart;
|
||||||
import com.vaadin.flow.component.notification.Notification;
|
import com.vaadin.flow.component.notification.Notification;
|
||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.HasDynamicTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Route("")
|
@Route("")
|
||||||
@PageTitle("Dashboard")
|
public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||||
public class DashboardView extends VerticalLayout {
|
|
||||||
|
|
||||||
public DashboardView() {
|
public DashboardView() {
|
||||||
addClassName("dialect-content");
|
addClassName("dialect-content");
|
||||||
|
|
||||||
|
List<String> months = List.of(
|
||||||
|
getTranslation("month.jan"), getTranslation("month.feb"),
|
||||||
|
getTranslation("month.mar"), getTranslation("month.apr"),
|
||||||
|
getTranslation("month.may"), getTranslation("month.jun"));
|
||||||
|
|
||||||
LineChart lineChart = new LineChart();
|
LineChart lineChart = new LineChart();
|
||||||
lineChart.setWidthFull();
|
lineChart.setWidthFull();
|
||||||
lineChart.setHeight("400px");
|
lineChart.setHeight("400px");
|
||||||
lineChart.setData("Umsatz 2026",
|
lineChart.setData(getTranslation("chart.revenueSeries"),
|
||||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
|
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
|
||||||
List.of("Jan", "Feb", "Mär", "Apr", "Mai", "Jun"));
|
months);
|
||||||
lineChart.addPointClickListener(e -> {
|
lineChart.addPointClickListener(e -> {
|
||||||
Notification.show("Serie %d, Punkt %d"
|
Notification.show(getTranslation("chart.pointClick",
|
||||||
.formatted(e.getSeriesIndex(), e.getDataPointIndex()));
|
e.getSeriesIndex(), e.getDataPointIndex()));
|
||||||
});
|
});
|
||||||
|
|
||||||
BarChart barChart = new BarChart();
|
BarChart barChart = new BarChart();
|
||||||
barChart.setWidthFull();
|
barChart.setWidthFull();
|
||||||
barChart.setHeight("400px");
|
barChart.setHeight("400px");
|
||||||
barChart.setData("Umsatz 2026",
|
barChart.setData(getTranslation("chart.revenueSeries"),
|
||||||
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
|
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0),
|
||||||
List.of("Jan", "Feb", "Mär", "Apr", "Mai", "Jun"));
|
months);
|
||||||
barChart.addPointClickListener(e -> {
|
barChart.addPointClickListener(e -> {
|
||||||
Notification.show("Serie %d, Punkt %d"
|
Notification.show(getTranslation("chart.pointClick",
|
||||||
.formatted(e.getSeriesIndex(), e.getDataPointIndex()));
|
e.getSeriesIndex(), e.getDataPointIndex()));
|
||||||
});
|
});
|
||||||
|
|
||||||
PieChart pieChart = new PieChart();
|
PieChart pieChart = new PieChart();
|
||||||
@@ -45,13 +49,19 @@ public class DashboardView extends VerticalLayout {
|
|||||||
pieChart.setHeight("400px");
|
pieChart.setHeight("400px");
|
||||||
pieChart.setData(
|
pieChart.setData(
|
||||||
List.of(30.0, 40.0, 35.0, 50.0),
|
List.of(30.0, 40.0, 35.0, 50.0),
|
||||||
List.of("Nord", "Süd", "Ost", "West"));
|
List.of(getTranslation("region.north"), getTranslation("region.south"),
|
||||||
|
getTranslation("region.east"), getTranslation("region.west")));
|
||||||
pieChart.addPointClickListener(e -> {
|
pieChart.addPointClickListener(e -> {
|
||||||
Notification.show("Slice %d".formatted(e.getDataPointIndex()));
|
Notification.show(getTranslation("chart.sliceClick", e.getDataPointIndex()));
|
||||||
});
|
});
|
||||||
|
|
||||||
add(new Card("Umsatz-Entwicklung", lineChart),
|
add(new Card(getTranslation("card.revenueTrend"), lineChart),
|
||||||
new Card("Umsatz nach Monat", barChart),
|
new Card(getTranslation("card.revenueByMonth"), barChart),
|
||||||
new Card("Umsatz nach Region", pieChart));
|
new Card(getTranslation("card.revenueByRegion"), pieChart));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPageTitle() {
|
||||||
|
return getTranslation("page.dashboard");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import com.vaadin.flow.component.timepicker.TimePicker;
|
|||||||
import com.vaadin.flow.data.binder.Binder;
|
import com.vaadin.flow.data.binder.Binder;
|
||||||
import com.vaadin.flow.data.validator.EmailValidator;
|
import com.vaadin.flow.data.validator.EmailValidator;
|
||||||
import com.vaadin.flow.data.validator.IntegerRangeValidator;
|
import com.vaadin.flow.data.validator.IntegerRangeValidator;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.HasDynamicTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
|
|
||||||
import java.text.DateFormatSymbols;
|
import java.text.DateFormatSymbols;
|
||||||
@@ -33,30 +33,34 @@ import java.util.Locale;
|
|||||||
* to {@link Registration} via {@link Binder}.
|
* to {@link Registration} via {@link Binder}.
|
||||||
*/
|
*/
|
||||||
@Route("formular")
|
@Route("formular")
|
||||||
@PageTitle("Formular")
|
public class FormView extends VerticalLayout implements HasDynamicTitle {
|
||||||
public class FormView extends VerticalLayout {
|
|
||||||
|
|
||||||
private final Binder<Registration> binder = new Binder<>(Registration.class);
|
private final Binder<Registration> binder = new Binder<>(Registration.class);
|
||||||
|
|
||||||
public FormView() {
|
public FormView() {
|
||||||
addClassName("dialect-content");
|
addClassName("dialect-content");
|
||||||
add(new Card("Registrierung", buildForm()));
|
add(new Card(getTranslation("card.registration"), buildForm()));
|
||||||
//<theme-editor-local-classname>
|
//<theme-editor-local-classname>
|
||||||
addClassName("form-view-vertical-layout-1");
|
addClassName("form-view-vertical-layout-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPageTitle() {
|
||||||
|
return getTranslation("page.form");
|
||||||
|
}
|
||||||
|
|
||||||
private Component buildForm() {
|
private Component buildForm() {
|
||||||
TextField vorname = new TextField("Vorname");
|
TextField vorname = new TextField(getTranslation("form.firstName"));
|
||||||
vorname.setAutofocus(true);
|
vorname.setAutofocus(true);
|
||||||
TextField nachname = new TextField("Nachname");
|
TextField nachname = new TextField(getTranslation("form.lastName"));
|
||||||
EmailField email = new EmailField("E-Mail");
|
EmailField email = new EmailField(getTranslation("form.email"));
|
||||||
PasswordField passwort = new PasswordField("Passwort");
|
PasswordField passwort = new PasswordField(getTranslation("form.password"));
|
||||||
//<theme-editor-local-classname>
|
//<theme-editor-local-classname>
|
||||||
passwort.addClassName("form-view-password-field-1");
|
passwort.addClassName("form-view-password-field-1");
|
||||||
IntegerField alter = new IntegerField("Alter");
|
IntegerField alter = new IntegerField(getTranslation("form.age"));
|
||||||
alter.setReadOnly(true);
|
alter.setReadOnly(true);
|
||||||
BigDecimalField gehalt = new BigDecimalField("Gehalt (€)");
|
BigDecimalField gehalt = new BigDecimalField(getTranslation("form.salary"));
|
||||||
DatePicker geburtsdatum = new DatePicker("Geburtsdatum");
|
DatePicker geburtsdatum = new DatePicker(getTranslation("form.birthdate"));
|
||||||
geburtsdatum.setLocale(Locale.GERMANY);
|
geburtsdatum.setLocale(Locale.GERMANY);
|
||||||
|
|
||||||
DateFormatSymbols symbols = new DateFormatSymbols(Locale.GERMANY);
|
DateFormatSymbols symbols = new DateFormatSymbols(Locale.GERMANY);
|
||||||
@@ -69,44 +73,48 @@ vorname.setAutofocus(true);
|
|||||||
.setWeekdays(wochentage)
|
.setWeekdays(wochentage)
|
||||||
.setWeekdaysShort(wochentageKurz)
|
.setWeekdaysShort(wochentageKurz)
|
||||||
.setFirstDayOfWeek(1)
|
.setFirstDayOfWeek(1)
|
||||||
.setToday("Heute")
|
.setToday(getTranslation("form.today"))
|
||||||
.setCancel("Abbrechen"));
|
.setCancel(getTranslation("form.cancel")));
|
||||||
TimePicker uhrzeit = new TimePicker("Bevorzugte Uhrzeit");
|
TimePicker uhrzeit = new TimePicker(getTranslation("form.time"));
|
||||||
|
|
||||||
ComboBox<String> land = new ComboBox<>("Land");
|
ComboBox<String> land = new ComboBox<>(getTranslation("form.country"));
|
||||||
land.setItems("Deutschland", "Österreich", "Schweiz", "Frankreich", "Niederlande");
|
land.setItems(getTranslation("country.de"), getTranslation("country.at"),
|
||||||
|
getTranslation("country.ch"), getTranslation("country.fr"), getTranslation("country.nl"));
|
||||||
|
|
||||||
Select<String> abteilung = new Select<>();
|
Select<String> abteilung = new Select<>();
|
||||||
abteilung.setLabel("Abteilung");
|
abteilung.setLabel(getTranslation("form.department"));
|
||||||
abteilung.setItems("Vertrieb", "Marketing", "Entwicklung", "Support", "Geschäftsführung");
|
abteilung.setItems(getTranslation("dept.sales"), getTranslation("dept.marketing"),
|
||||||
|
getTranslation("dept.dev"), getTranslation("dept.support"), getTranslation("dept.management"));
|
||||||
|
|
||||||
CheckboxGroup<String> interessen = new CheckboxGroup<>();
|
CheckboxGroup<String> interessen = new CheckboxGroup<>();
|
||||||
interessen.setLabel("Interessen");
|
interessen.setLabel(getTranslation("form.interests"));
|
||||||
interessen.setItems("Newsletter-Themen", "Webinare", "Produkt-Updates", "Events");
|
interessen.setItems(getTranslation("interest.newsletter"), getTranslation("interest.webinars"),
|
||||||
|
getTranslation("interest.updates"), getTranslation("interest.events"));
|
||||||
|
|
||||||
RadioButtonGroup<String> geschlecht = new RadioButtonGroup<>();
|
RadioButtonGroup<String> geschlecht = new RadioButtonGroup<>();
|
||||||
geschlecht.setLabel("Anrede");
|
geschlecht.setLabel(getTranslation("form.salutation"));
|
||||||
geschlecht.setItems("Frau", "Herr", "Divers", "Keine Angabe");
|
geschlecht.setItems(getTranslation("salutation.mrs"), getTranslation("salutation.mr"),
|
||||||
|
getTranslation("salutation.diverse"), getTranslation("salutation.none"));
|
||||||
|
|
||||||
Checkbox newsletter = new Checkbox("Newsletter abonnieren");
|
Checkbox newsletter = new Checkbox(getTranslation("form.newsletter"));
|
||||||
|
|
||||||
TextArea bemerkung = new TextArea("Bemerkung");
|
TextArea bemerkung = new TextArea(getTranslation("form.remark"));
|
||||||
bemerkung.setMaxLength(500);
|
bemerkung.setMaxLength(500);
|
||||||
|
|
||||||
binder.forField(vorname)
|
binder.forField(vorname)
|
||||||
.asRequired("Vorname wird benötigt")
|
.asRequired(getTranslation("valid.firstNameRequired"))
|
||||||
.bind(Registration::getVorname, Registration::setVorname);
|
.bind(Registration::getVorname, Registration::setVorname);
|
||||||
binder.forField(nachname)
|
binder.forField(nachname)
|
||||||
.asRequired("Nachname wird benötigt")
|
.asRequired(getTranslation("valid.lastNameRequired"))
|
||||||
.bind(Registration::getNachname, Registration::setNachname);
|
.bind(Registration::getNachname, Registration::setNachname);
|
||||||
binder.forField(email)
|
binder.forField(email)
|
||||||
.asRequired("E-Mail wird benötigt")
|
.asRequired(getTranslation("valid.emailRequired"))
|
||||||
.withValidator(new EmailValidator("Ungültige E-Mail-Adresse"))
|
.withValidator(new EmailValidator(getTranslation("valid.emailInvalid")))
|
||||||
.bind(Registration::getEmail, Registration::setEmail);
|
.bind(Registration::getEmail, Registration::setEmail);
|
||||||
binder.forField(passwort)
|
binder.forField(passwort)
|
||||||
.bind(Registration::getPasswort, Registration::setPasswort);
|
.bind(Registration::getPasswort, Registration::setPasswort);
|
||||||
binder.forField(alter)
|
binder.forField(alter)
|
||||||
.withValidator(new IntegerRangeValidator("Alter muss zwischen 0 und 120 liegen", 0, 120))
|
.withValidator(new IntegerRangeValidator(getTranslation("valid.ageRange"), 0, 120))
|
||||||
.bind(Registration::getAlter, Registration::setAlter);
|
.bind(Registration::getAlter, Registration::setAlter);
|
||||||
binder.forField(gehalt)
|
binder.forField(gehalt)
|
||||||
.bind(Registration::getGehalt, Registration::setGehalt);
|
.bind(Registration::getGehalt, Registration::setGehalt);
|
||||||
@@ -143,20 +151,20 @@ vorname.setAutofocus(true);
|
|||||||
formLayout.setColspan(bemerkung, 2);
|
formLayout.setColspan(bemerkung, 2);
|
||||||
formLayout.add(interessen, geschlecht, newsletter, bemerkung);
|
formLayout.add(interessen, geschlecht, newsletter, bemerkung);
|
||||||
|
|
||||||
Button speichern = new Button("Speichern", e -> {
|
Button speichern = new Button(getTranslation("form.save"), e -> {
|
||||||
Registration ziel = new Registration();
|
Registration ziel = new Registration();
|
||||||
if (binder.writeBeanIfValid(ziel)) {
|
if (binder.writeBeanIfValid(ziel)) {
|
||||||
Notification erfolg = Notification.show(
|
Notification erfolg = Notification.show(
|
||||||
"Gespeichert: %s %s".formatted(ziel.getVorname(), ziel.getNachname()));
|
getTranslation("notify.saved", ziel.getVorname(), ziel.getNachname()));
|
||||||
erfolg.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
erfolg.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
|
||||||
} else {
|
} else {
|
||||||
Notification fehler = Notification.show("Bitte Eingaben prüfen");
|
Notification fehler = Notification.show(getTranslation("notify.checkInput"));
|
||||||
fehler.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
fehler.addThemeVariants(NotificationVariant.LUMO_ERROR);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
speichern.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
speichern.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
|
||||||
|
|
||||||
Button zuruecksetzen = new Button("Zurücksetzen",
|
Button zuruecksetzen = new Button(getTranslation("form.reset"),
|
||||||
e -> binder.readBean(new Registration()));
|
e -> binder.readBean(new Registration()));
|
||||||
|
|
||||||
HorizontalLayout buttons = new HorizontalLayout(speichern, zuruecksetzen);
|
HorizontalLayout buttons = new HorizontalLayout(speichern, zuruecksetzen);
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
package com.example.views;
|
package com.example.views;
|
||||||
|
|
||||||
import com.example.js.CommonJS;
|
import com.example.js.CommonJS;
|
||||||
|
import com.vaadin.flow.component.UI;
|
||||||
import com.vaadin.flow.component.applayout.AppLayout;
|
import com.vaadin.flow.component.applayout.AppLayout;
|
||||||
import com.vaadin.flow.component.applayout.DrawerToggle;
|
import com.vaadin.flow.component.applayout.DrawerToggle;
|
||||||
import com.vaadin.flow.component.button.Button;
|
import com.vaadin.flow.component.button.Button;
|
||||||
import com.vaadin.flow.component.button.ButtonVariant;
|
import com.vaadin.flow.component.button.ButtonVariant;
|
||||||
import com.vaadin.flow.component.html.Span;
|
import com.vaadin.flow.component.html.Span;
|
||||||
import com.vaadin.flow.component.icon.VaadinIcon;
|
import com.vaadin.flow.component.icon.VaadinIcon;
|
||||||
|
import com.vaadin.flow.component.menubar.MenuBar;
|
||||||
|
import com.vaadin.flow.component.menubar.MenuBarVariant;
|
||||||
import com.vaadin.flow.component.sidenav.SideNav;
|
import com.vaadin.flow.component.sidenav.SideNav;
|
||||||
import com.vaadin.flow.component.sidenav.SideNavItem;
|
import com.vaadin.flow.component.sidenav.SideNavItem;
|
||||||
import com.vaadin.flow.router.Layout;
|
import com.vaadin.flow.router.Layout;
|
||||||
|
import com.vaadin.flow.server.VaadinSession;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
@Layout
|
@Layout
|
||||||
public class MainLayout extends AppLayout {
|
public class MainLayout extends AppLayout {
|
||||||
@@ -19,32 +25,53 @@ public class MainLayout extends AppLayout {
|
|||||||
public MainLayout() {
|
public MainLayout() {
|
||||||
DrawerToggle toggle = new DrawerToggle();
|
DrawerToggle toggle = new DrawerToggle();
|
||||||
|
|
||||||
Span title = new Span("Chart App");
|
Span title = new Span(getTranslation("app.title"));
|
||||||
title.addClassName("dialect-title");
|
title.addClassName("dialect-title");
|
||||||
|
|
||||||
Span spacer = new Span();
|
Span spacer = new Span();
|
||||||
spacer.getStyle().set("flex-grow", "1");
|
spacer.getStyle().set("flex-grow", "1");
|
||||||
|
|
||||||
|
MenuBar languageMenu = buildLanguageMenu();
|
||||||
|
|
||||||
themeToggle.addThemeVariants(ButtonVariant.TERTIARY, ButtonVariant.LUMO_ICON);
|
themeToggle.addThemeVariants(ButtonVariant.TERTIARY, ButtonVariant.LUMO_ICON);
|
||||||
themeToggle.setAriaLabel("Farbschema wechseln");
|
themeToggle.setAriaLabel(getTranslation("action.themeToggle"));
|
||||||
themeToggle.addClickListener(e ->
|
themeToggle.addClickListener(e ->
|
||||||
getElement().executeJs(CommonJS.TOGGLE_THEME_JS).then(String.class, this::applyIconFor));
|
getElement().executeJs(CommonJS.TOGGLE_THEME_JS).then(String.class, this::applyIconFor));
|
||||||
|
|
||||||
addToNavbar(toggle, title, spacer, themeToggle);
|
addToNavbar(toggle, title, spacer, languageMenu, themeToggle);
|
||||||
|
|
||||||
getElement().executeJs(CommonJS.INIT_THEME_JS).then(String.class, this::applyIconFor);
|
getElement().executeJs(CommonJS.INIT_THEME_JS).then(String.class, this::applyIconFor);
|
||||||
|
|
||||||
SideNav nav = new SideNav();
|
SideNav nav = new SideNav();
|
||||||
nav.addItem(new SideNavItem("Dashboard", DashboardView.class,
|
nav.addItem(new SideNavItem(getTranslation("nav.dashboard"), DashboardView.class,
|
||||||
VaadinIcon.DASHBOARD.create()));
|
VaadinIcon.DASHBOARD.create()));
|
||||||
nav.addItem(new SideNavItem("Formular", FormView.class,
|
nav.addItem(new SideNavItem(getTranslation("nav.form"), FormView.class,
|
||||||
VaadinIcon.FORM.create()));
|
VaadinIcon.FORM.create()));
|
||||||
nav.addItem(new SideNavItem("Tabelle", TableView.class,
|
nav.addItem(new SideNavItem(getTranslation("nav.table"), TableView.class,
|
||||||
VaadinIcon.TABLE.create()));
|
VaadinIcon.TABLE.create()));
|
||||||
|
|
||||||
addToDrawer(nav);
|
addToDrawer(nav);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MenuBar buildLanguageMenu() {
|
||||||
|
MenuBar menuBar = new MenuBar();
|
||||||
|
menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE);
|
||||||
|
|
||||||
|
var root = menuBar.addItem(VaadinIcon.GLOBE_WIRE.create());
|
||||||
|
root.setAriaLabel(getTranslation("action.language"));
|
||||||
|
|
||||||
|
root.getSubMenu().addItem(getTranslation("lang.de"), e -> switchLanguage(Locale.GERMAN));
|
||||||
|
root.getSubMenu().addItem(getTranslation("lang.en"), e -> switchLanguage(Locale.ENGLISH));
|
||||||
|
root.getSubMenu().addItem(getTranslation("lang.es"), e -> switchLanguage(Locale.of("es")));
|
||||||
|
|
||||||
|
return menuBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void switchLanguage(Locale locale) {
|
||||||
|
VaadinSession.getCurrent().setLocale(locale);
|
||||||
|
UI.getCurrent().getPage().reload();
|
||||||
|
}
|
||||||
|
|
||||||
private void applyIconFor(String theme) {
|
private void applyIconFor(String theme) {
|
||||||
themeToggle.setIcon("dark".equals(theme) ? VaadinIcon.SUN_O.create() : VaadinIcon.MOON.create());
|
themeToggle.setIcon("dark".equals(theme) ? VaadinIcon.SUN_O.create() : VaadinIcon.MOON.create());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.vaadin.flow.component.icon.VaadinIcon;
|
|||||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||||
import com.vaadin.flow.component.textfield.TextField;
|
import com.vaadin.flow.component.textfield.TextField;
|
||||||
import com.vaadin.flow.data.value.ValueChangeMode;
|
import com.vaadin.flow.data.value.ValueChangeMode;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.HasDynamicTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
@@ -19,8 +19,7 @@ import java.util.Locale;
|
|||||||
* live text filter across all columns.
|
* live text filter across all columns.
|
||||||
*/
|
*/
|
||||||
@Route("tabelle")
|
@Route("tabelle")
|
||||||
@PageTitle("Tabelle")
|
public class TableView extends VerticalLayout implements HasDynamicTitle {
|
||||||
public class TableView extends VerticalLayout {
|
|
||||||
|
|
||||||
private record Mitarbeiter(String name, String abteilung, String stadt, double gehalt) {
|
private record Mitarbeiter(String name, String abteilung, String stadt, double gehalt) {
|
||||||
}
|
}
|
||||||
@@ -45,7 +44,7 @@ public class TableView extends VerticalLayout {
|
|||||||
addClassName("dialect-content");
|
addClassName("dialect-content");
|
||||||
|
|
||||||
TextField suche = new TextField();
|
TextField suche = new TextField();
|
||||||
suche.setPlaceholder("Suchen…");
|
suche.setPlaceholder(getTranslation("table.search"));
|
||||||
suche.setPrefixComponent(VaadinIcon.SEARCH.create());
|
suche.setPrefixComponent(VaadinIcon.SEARCH.create());
|
||||||
suche.setClearButtonVisible(true);
|
suche.setClearButtonVisible(true);
|
||||||
suche.setValueChangeMode(ValueChangeMode.EAGER);
|
suche.setValueChangeMode(ValueChangeMode.EAGER);
|
||||||
@@ -53,10 +52,10 @@ public class TableView extends VerticalLayout {
|
|||||||
|
|
||||||
Grid<Mitarbeiter> grid = new Grid<>();
|
Grid<Mitarbeiter> grid = new Grid<>();
|
||||||
grid.setWidthFull();
|
grid.setWidthFull();
|
||||||
grid.addColumn(Mitarbeiter::name).setHeader("Name").setSortable(true);
|
grid.addColumn(Mitarbeiter::name).setHeader(getTranslation("col.name")).setSortable(true);
|
||||||
grid.addColumn(Mitarbeiter::abteilung).setHeader("Abteilung").setSortable(true);
|
grid.addColumn(Mitarbeiter::abteilung).setHeader(getTranslation("col.department")).setSortable(true);
|
||||||
grid.addColumn(Mitarbeiter::stadt).setHeader("Stadt").setSortable(true);
|
grid.addColumn(Mitarbeiter::stadt).setHeader(getTranslation("col.city")).setSortable(true);
|
||||||
grid.addColumn(m -> GEHALT_FORMAT.format(m.gehalt())).setHeader("Gehalt (€)").setSortable(true);
|
grid.addColumn(m -> GEHALT_FORMAT.format(m.gehalt())).setHeader(getTranslation("col.salary")).setSortable(true);
|
||||||
|
|
||||||
GridListDataView<Mitarbeiter> dataView = grid.setItems(MITARBEITER);
|
GridListDataView<Mitarbeiter> dataView = grid.setItems(MITARBEITER);
|
||||||
suche.addValueChangeListener(e -> dataView.refreshAll());
|
suche.addValueChangeListener(e -> dataView.refreshAll());
|
||||||
@@ -66,7 +65,12 @@ public class TableView extends VerticalLayout {
|
|||||||
content.setPadding(false);
|
content.setPadding(false);
|
||||||
content.setSpacing(true);
|
content.setSpacing(true);
|
||||||
|
|
||||||
add(new Card("Mitarbeiter", content));
|
add(new Card(getTranslation("card.employees"), content));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPageTitle() {
|
||||||
|
return getTranslation("page.table");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean matches(Mitarbeiter mitarbeiter, String filter) {
|
private static boolean matches(Mitarbeiter mitarbeiter, String filter) {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# German (source of truth / DefaultI18NProvider fallback)
|
||||||
|
|
||||||
|
app.title=Chart App
|
||||||
|
action.themeToggle=Farbschema wechseln
|
||||||
|
action.language=Sprache wechseln
|
||||||
|
lang.de=Deutsch
|
||||||
|
lang.en=English
|
||||||
|
lang.es=Español
|
||||||
|
|
||||||
|
nav.dashboard=Dashboard
|
||||||
|
nav.form=Formular
|
||||||
|
nav.table=Tabelle
|
||||||
|
|
||||||
|
page.dashboard=Dashboard
|
||||||
|
page.form=Formular
|
||||||
|
page.table=Tabelle
|
||||||
|
|
||||||
|
card.revenueTrend=Umsatz-Entwicklung
|
||||||
|
card.revenueByMonth=Umsatz nach Monat
|
||||||
|
card.revenueByRegion=Umsatz nach Region
|
||||||
|
card.registration=Registrierung
|
||||||
|
card.employees=Mitarbeiter
|
||||||
|
|
||||||
|
chart.revenueSeries=Umsatz 2026
|
||||||
|
chart.pointClick=Serie {0}, Punkt {1}
|
||||||
|
chart.sliceClick=Slice {0}
|
||||||
|
|
||||||
|
month.jan=Jan
|
||||||
|
month.feb=Feb
|
||||||
|
month.mar=Mär
|
||||||
|
month.apr=Apr
|
||||||
|
month.may=Mai
|
||||||
|
month.jun=Jun
|
||||||
|
|
||||||
|
region.north=Nord
|
||||||
|
region.south=Süd
|
||||||
|
region.east=Ost
|
||||||
|
region.west=West
|
||||||
|
|
||||||
|
form.firstName=Vorname
|
||||||
|
form.lastName=Nachname
|
||||||
|
form.email=E-Mail
|
||||||
|
form.password=Passwort
|
||||||
|
form.age=Alter
|
||||||
|
form.salary=Gehalt (€)
|
||||||
|
form.birthdate=Geburtsdatum
|
||||||
|
form.time=Bevorzugte Uhrzeit
|
||||||
|
form.country=Land
|
||||||
|
form.department=Abteilung
|
||||||
|
form.interests=Interessen
|
||||||
|
form.salutation=Anrede
|
||||||
|
form.newsletter=Newsletter abonnieren
|
||||||
|
form.remark=Bemerkung
|
||||||
|
form.save=Speichern
|
||||||
|
form.reset=Zurücksetzen
|
||||||
|
form.today=Heute
|
||||||
|
form.cancel=Abbrechen
|
||||||
|
|
||||||
|
country.de=Deutschland
|
||||||
|
country.at=Österreich
|
||||||
|
country.ch=Schweiz
|
||||||
|
country.fr=Frankreich
|
||||||
|
country.nl=Niederlande
|
||||||
|
|
||||||
|
dept.sales=Vertrieb
|
||||||
|
dept.marketing=Marketing
|
||||||
|
dept.dev=Entwicklung
|
||||||
|
dept.support=Support
|
||||||
|
dept.management=Geschäftsführung
|
||||||
|
|
||||||
|
interest.newsletter=Newsletter-Themen
|
||||||
|
interest.webinars=Webinare
|
||||||
|
interest.updates=Produkt-Updates
|
||||||
|
interest.events=Events
|
||||||
|
|
||||||
|
salutation.mrs=Frau
|
||||||
|
salutation.mr=Herr
|
||||||
|
salutation.diverse=Divers
|
||||||
|
salutation.none=Keine Angabe
|
||||||
|
|
||||||
|
valid.firstNameRequired=Vorname wird benötigt
|
||||||
|
valid.lastNameRequired=Nachname wird benötigt
|
||||||
|
valid.emailRequired=E-Mail wird benötigt
|
||||||
|
valid.emailInvalid=Ungültige E-Mail-Adresse
|
||||||
|
valid.ageRange=Alter muss zwischen 0 und 120 liegen
|
||||||
|
|
||||||
|
notify.saved=Gespeichert: {0} {1}
|
||||||
|
notify.checkInput=Bitte Eingaben prüfen
|
||||||
|
|
||||||
|
table.search=Suchen…
|
||||||
|
col.name=Name
|
||||||
|
col.department=Abteilung
|
||||||
|
col.city=Stadt
|
||||||
|
col.salary=Gehalt (€)
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# English
|
||||||
|
|
||||||
|
app.title=Chart App
|
||||||
|
action.themeToggle=Switch color scheme
|
||||||
|
action.language=Switch language
|
||||||
|
lang.de=Deutsch
|
||||||
|
lang.en=English
|
||||||
|
lang.es=Español
|
||||||
|
|
||||||
|
nav.dashboard=Dashboard
|
||||||
|
nav.form=Form
|
||||||
|
nav.table=Table
|
||||||
|
|
||||||
|
page.dashboard=Dashboard
|
||||||
|
page.form=Form
|
||||||
|
page.table=Table
|
||||||
|
|
||||||
|
card.revenueTrend=Revenue Trend
|
||||||
|
card.revenueByMonth=Revenue by Month
|
||||||
|
card.revenueByRegion=Revenue by Region
|
||||||
|
card.registration=Registration
|
||||||
|
card.employees=Employees
|
||||||
|
|
||||||
|
chart.revenueSeries=Revenue 2026
|
||||||
|
chart.pointClick=Series {0}, point {1}
|
||||||
|
chart.sliceClick=Slice {0}
|
||||||
|
|
||||||
|
month.jan=Jan
|
||||||
|
month.feb=Feb
|
||||||
|
month.mar=Mar
|
||||||
|
month.apr=Apr
|
||||||
|
month.may=May
|
||||||
|
month.jun=Jun
|
||||||
|
|
||||||
|
region.north=North
|
||||||
|
region.south=South
|
||||||
|
region.east=East
|
||||||
|
region.west=West
|
||||||
|
|
||||||
|
form.firstName=First name
|
||||||
|
form.lastName=Last name
|
||||||
|
form.email=Email
|
||||||
|
form.password=Password
|
||||||
|
form.age=Age
|
||||||
|
form.salary=Salary (€)
|
||||||
|
form.birthdate=Date of birth
|
||||||
|
form.time=Preferred time
|
||||||
|
form.country=Country
|
||||||
|
form.department=Department
|
||||||
|
form.interests=Interests
|
||||||
|
form.salutation=Salutation
|
||||||
|
form.newsletter=Subscribe to newsletter
|
||||||
|
form.remark=Remark
|
||||||
|
form.save=Save
|
||||||
|
form.reset=Reset
|
||||||
|
form.today=Today
|
||||||
|
form.cancel=Cancel
|
||||||
|
|
||||||
|
country.de=Germany
|
||||||
|
country.at=Austria
|
||||||
|
country.ch=Switzerland
|
||||||
|
country.fr=France
|
||||||
|
country.nl=Netherlands
|
||||||
|
|
||||||
|
dept.sales=Sales
|
||||||
|
dept.marketing=Marketing
|
||||||
|
dept.dev=Development
|
||||||
|
dept.support=Support
|
||||||
|
dept.management=Management
|
||||||
|
|
||||||
|
interest.newsletter=Newsletter topics
|
||||||
|
interest.webinars=Webinars
|
||||||
|
interest.updates=Product updates
|
||||||
|
interest.events=Events
|
||||||
|
|
||||||
|
salutation.mrs=Ms.
|
||||||
|
salutation.mr=Mr.
|
||||||
|
salutation.diverse=Diverse
|
||||||
|
salutation.none=Not specified
|
||||||
|
|
||||||
|
valid.firstNameRequired=First name is required
|
||||||
|
valid.lastNameRequired=Last name is required
|
||||||
|
valid.emailRequired=Email is required
|
||||||
|
valid.emailInvalid=Invalid email address
|
||||||
|
valid.ageRange=Age must be between 0 and 120
|
||||||
|
|
||||||
|
notify.saved=Saved: {0} {1}
|
||||||
|
notify.checkInput=Please check your entries
|
||||||
|
|
||||||
|
table.search=Search…
|
||||||
|
col.name=Name
|
||||||
|
col.department=Department
|
||||||
|
col.city=City
|
||||||
|
col.salary=Salary (€)
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Spanish
|
||||||
|
|
||||||
|
app.title=Chart App
|
||||||
|
action.themeToggle=Cambiar esquema de color
|
||||||
|
action.language=Cambiar idioma
|
||||||
|
lang.de=Deutsch
|
||||||
|
lang.en=English
|
||||||
|
lang.es=Español
|
||||||
|
|
||||||
|
nav.dashboard=Panel
|
||||||
|
nav.form=Formulario
|
||||||
|
nav.table=Tabla
|
||||||
|
|
||||||
|
page.dashboard=Panel
|
||||||
|
page.form=Formulario
|
||||||
|
page.table=Tabla
|
||||||
|
|
||||||
|
card.revenueTrend=Evolución de ingresos
|
||||||
|
card.revenueByMonth=Ingresos por mes
|
||||||
|
card.revenueByRegion=Ingresos por región
|
||||||
|
card.registration=Registro
|
||||||
|
card.employees=Empleados
|
||||||
|
|
||||||
|
chart.revenueSeries=Ingresos 2026
|
||||||
|
chart.pointClick=Serie {0}, punto {1}
|
||||||
|
chart.sliceClick=Sector {0}
|
||||||
|
|
||||||
|
month.jan=Ene
|
||||||
|
month.feb=Feb
|
||||||
|
month.mar=Mar
|
||||||
|
month.apr=Abr
|
||||||
|
month.may=May
|
||||||
|
month.jun=Jun
|
||||||
|
|
||||||
|
region.north=Norte
|
||||||
|
region.south=Sur
|
||||||
|
region.east=Este
|
||||||
|
region.west=Oeste
|
||||||
|
|
||||||
|
form.firstName=Nombre
|
||||||
|
form.lastName=Apellido
|
||||||
|
form.email=Correo electrónico
|
||||||
|
form.password=Contraseña
|
||||||
|
form.age=Edad
|
||||||
|
form.salary=Salario (€)
|
||||||
|
form.birthdate=Fecha de nacimiento
|
||||||
|
form.time=Hora preferida
|
||||||
|
form.country=País
|
||||||
|
form.department=Departamento
|
||||||
|
form.interests=Intereses
|
||||||
|
form.salutation=Tratamiento
|
||||||
|
form.newsletter=Suscribirse al boletín
|
||||||
|
form.remark=Observación
|
||||||
|
form.save=Guardar
|
||||||
|
form.reset=Restablecer
|
||||||
|
form.today=Hoy
|
||||||
|
form.cancel=Cancelar
|
||||||
|
|
||||||
|
country.de=Alemania
|
||||||
|
country.at=Austria
|
||||||
|
country.ch=Suiza
|
||||||
|
country.fr=Francia
|
||||||
|
country.nl=Países Bajos
|
||||||
|
|
||||||
|
dept.sales=Ventas
|
||||||
|
dept.marketing=Marketing
|
||||||
|
dept.dev=Desarrollo
|
||||||
|
dept.support=Soporte
|
||||||
|
dept.management=Dirección
|
||||||
|
|
||||||
|
interest.newsletter=Temas del boletín
|
||||||
|
interest.webinars=Seminarios web
|
||||||
|
interest.updates=Actualizaciones de producto
|
||||||
|
interest.events=Eventos
|
||||||
|
|
||||||
|
salutation.mrs=Sra.
|
||||||
|
salutation.mr=Sr.
|
||||||
|
salutation.diverse=Diverso
|
||||||
|
salutation.none=Sin especificar
|
||||||
|
|
||||||
|
valid.firstNameRequired=El nombre es obligatorio
|
||||||
|
valid.lastNameRequired=El apellido es obligatorio
|
||||||
|
valid.emailRequired=El correo electrónico es obligatorio
|
||||||
|
valid.emailInvalid=Dirección de correo no válida
|
||||||
|
valid.ageRange=La edad debe estar entre 0 y 120
|
||||||
|
|
||||||
|
notify.saved=Guardado: {0} {1}
|
||||||
|
notify.checkInput=Por favor revise los datos ingresados
|
||||||
|
|
||||||
|
table.search=Buscar…
|
||||||
|
col.name=Nombre
|
||||||
|
col.department=Departamento
|
||||||
|
col.city=Ciudad
|
||||||
|
col.salary=Salario (€)
|
||||||
Reference in New Issue
Block a user