diff --git a/src/main/bundles/dev.bundle b/src/main/bundles/dev.bundle index f9830f4..68e8d02 100644 Binary files a/src/main/bundles/dev.bundle and b/src/main/bundles/dev.bundle differ diff --git a/src/main/frontend/themes/votian-modern/styles.css b/src/main/frontend/themes/votian-modern/styles.css new file mode 100644 index 0000000..3ab9bb4 --- /dev/null +++ b/src/main/frontend/themes/votian-modern/styles.css @@ -0,0 +1,1726 @@ +@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap"); + +html { + --app-accent: #2563eb; + --app-accent-strong: #1d4ed8; + --app-accent-soft: rgba(37, 99, 235, 0.12); + --app-success: #059669; + --app-warning: #d97706; + --app-danger: #dc2626; + --app-surface: rgba(255, 255, 255, 0.78); + --app-surface-solid: #ffffff; + --app-surface-muted: rgba(247, 250, 255, 0.88); + --app-border: rgba(148, 163, 184, 0.18); + --app-border-strong: rgba(148, 163, 184, 0.28); + --app-field-border: #d6dde7; + --app-field-border-hover: #c6d0dd; + --app-text-strong: #0f172a; + --app-text: #1e293b; + --app-text-muted: #64748b; + --app-sidebar-background: linear-gradient(180deg, #081224 0%, #102a54 48%, #0f4c5c 100%); + --app-shell-background: + radial-gradient(circle at top left, rgba(37, 99, 235, 0.16), transparent 32%), + radial-gradient(circle at bottom right, rgba(20, 184, 166, 0.16), transparent 28%), + linear-gradient(180deg, #eef4ff 0%, #f7f9fc 45%, #eef2f7 100%); + --app-shadow-sm: 0 12px 28px rgba(15, 23, 42, 0.08); + --app-shadow-md: 0 20px 44px rgba(15, 23, 42, 0.1); + --app-shadow-lg: 0 28px 72px rgba(15, 23, 42, 0.12); + --app-shadow-xl: 0 36px 88px rgba(8, 18, 36, 0.24); + --lumo-font-family: "Manrope", "Avenir Next", "Segoe UI", sans-serif; + --lumo-base-color: #f5f7fb; + --lumo-body-text-color: var(--app-text); + --lumo-header-text-color: var(--app-text-strong); + --lumo-secondary-text-color: var(--app-text-muted); + --lumo-tertiary-text-color: #94a3b8; + --lumo-primary-color: #2563eb; + --lumo-primary-color-10pct: rgba(37, 99, 235, 0.12); + --lumo-primary-color-50pct: rgba(37, 99, 235, 0.42); + --lumo-primary-text-color: #1d4ed8; + --lumo-success-color: #059669; + --lumo-success-color-10pct: rgba(5, 150, 105, 0.12); + --lumo-success-text-color: #047857; + --lumo-error-color: #dc2626; + --lumo-error-color-10pct: rgba(220, 38, 38, 0.12); + --lumo-error-text-color: #b91c1c; + --lumo-border-radius-s: 14px; + --lumo-border-radius-m: 18px; + --lumo-border-radius-l: 24px; + --lumo-border-radius-xl: 32px; + --lumo-box-shadow-xs: 0 8px 18px rgba(15, 23, 42, 0.06); + --lumo-box-shadow-s: var(--app-shadow-sm); + --lumo-box-shadow-m: var(--app-shadow-md); + --lumo-box-shadow-l: var(--app-shadow-lg); + color-scheme: light; +} + +html, +body { + margin: 0; + padding: 0; + overflow-x: hidden; + max-width: 100%; +} + +body { + background: var(--app-shell-background); + color: var(--app-text); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--app-text-strong); + letter-spacing: -0.03em; +} + +p { + color: var(--app-text-muted); + line-height: 1.7; +} + +a { + color: var(--app-accent-strong); +} + +vaadin-app-layout { + --vaadin-app-layout-drawer-width: 336px; + background: transparent; +} + +vaadin-app-layout::part(drawer) { + background: var(--app-sidebar-background); + border-right: 0; + box-shadow: 24px 0 60px rgba(8, 18, 36, 0.18); + display: flex; + flex-direction: column; + height: 100dvh; + overflow: hidden; +} + +vaadin-app-layout::part(navbar) { + background: transparent; + border: 0; + box-shadow: none; + min-height: 0; +} + +vaadin-app-layout::part(content) { + background: transparent; + padding: 0; + overflow: hidden; + min-width: 0; + box-sizing: border-box; +} + +.view-container { + display: flex; + flex-direction: column; + background: red; + margin: 20px; + width: calc(100% - 40px); + max-width: calc(100% - 40px); + min-width: 0; + height: calc(100dvh - 40px); + overflow: hidden; + border-radius: 12px; + box-sizing: border-box; +} + +.view-container > * { + flex: 1 1 0; + min-height: 0; + min-width: 0; + max-width: 100%; + box-sizing: border-box; +} + +.page-shell { + width: 100%; + margin: 0; +} + +.surface-panel { + border: 1px solid var(--app-border-strong); + background: var(--app-surface); + backdrop-filter: blur(18px); + box-shadow: var(--app-shadow-lg); + border-radius: 28px; + box-sizing: border-box; + max-width: 100%; +} + +.landing-view, +.dashboard-home-view, +.login-view { + box-sizing: border-box; + padding: 0; + gap: 1rem; + background: transparent; +} + +.data-view, +.dashboard-view, +.form-page, +.message-hub-view, +.statistics-chat-view, +.admin-form-view { + box-sizing: border-box; + width: 100%; + max-width: 100%; + min-width: 0; + margin: 0; + padding: 0; + gap: 1rem; + overflow-x: hidden; +} + +.form-shell { + width: 100%; + max-width: 100%; + min-width: 0; + margin: 0; + box-sizing: border-box; +} + +/* Override Lumo's vaadin-vertical-layout[theme~="padding"] (specificity 0,0,1,1) + for full-width wrappers — .form-card and .surface-panel keep their own padding */ +vaadin-vertical-layout.form-shell:not(.form-card):not(.surface-panel), +vaadin-vertical-layout.form-page, +vaadin-vertical-layout.data-view, +vaadin-vertical-layout.admin-form-view { + padding: 0; +} + +/* Override setJustifyContentMode(CENTER) inline style on data/admin views + so content aligns to the top instead of being vertically centered */ +vaadin-vertical-layout.data-view, +vaadin-vertical-layout.admin-form-view { + justify-content: flex-start !important; + align-items: stretch !important; +} + +.form-shell.narrow { + width: 100%; +} + +.landing-header, +.section-panel, +.app-overview-panel, +.footer-panel, +.dashboard-header, +.dashboard-section, +.view-toolbar, +.form-card, +.message-section, +.message-thread-layout, +.statistics-header, +.statistics-input-panel { + padding: clamp(1rem, 1.8vw, 1.5rem); +} + +.form-card { + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(18px); + box-shadow: var(--app-shadow-lg); + border-radius: 28px; +} + +.form-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.form-title { + margin: 0; + font-size: clamp(1.55rem, 3vw, 2.2rem); + font-weight: 800; + letter-spacing: -0.05em; + color: var(--app-text-strong); +} + +.form-subtitle { + margin: 0; + color: var(--app-text-muted); +} + +.form-card .vaadin-form-layout, +.form-card vaadin-form-layout { + width: 100%; +} + +.section-card, +.content-card, +.simple-card { + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.88); + box-shadow: var(--app-shadow-md); + border-radius: 26px; +} + +.section-card, +.content-card { + padding: 1.15rem; +} + +.simple-card { + padding: 1rem; +} + +.landing-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; +} + +.landing-logo { + margin: 0; + font-size: clamp(1.6rem, 4vw, 2.2rem); + font-weight: 800; + letter-spacing: -0.05em; + color: var(--app-text-strong); +} + +.landing-nav { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.75rem; +} + +.landing-nav-button, +.landing-language-button { + border-radius: 999px; +} + +.hero-panel { + position: relative; + overflow: hidden; + min-height: 340px; + justify-content: center; + text-align: center; + border-radius: 34px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: linear-gradient(135deg, #081224 0%, #1d4ed8 54%, #0f766e 100%); + box-shadow: var(--app-shadow-xl); +} + +.hero-panel::before, +.hero-panel::after, +.login-highlight::before, +.login-highlight::after { + content: ""; + position: absolute; + border-radius: 999px; + pointer-events: none; +} + +.hero-panel::before, +.login-highlight::before { + width: 320px; + height: 320px; + top: -120px; + right: -100px; + background: radial-gradient(circle, rgba(255, 255, 255, 0.2), transparent 68%); +} + +.hero-panel::after, +.login-highlight::after { + width: 260px; + height: 260px; + bottom: -120px; + left: -80px; + background: radial-gradient(circle, rgba(147, 197, 253, 0.22), transparent 72%); +} + +.hero-panel > * , +.login-highlight > * { + position: relative; + z-index: 1; +} + +.hero-panel-title, +.login-highlight-title { + margin: 0; + color: #f8fafc; + font-size: clamp(2rem, 5vw, 3.4rem); + font-weight: 800; + letter-spacing: -0.06em; +} + +.hero-panel-text, +.login-highlight-text { + max-width: 760px; + margin: 0; + color: rgba(226, 232, 240, 0.92); + font-size: clamp(1rem, 2vw, 1.15rem); +} + +.hero-panel-icon { + color: rgba(248, 250, 252, 0.94); + filter: drop-shadow(0 18px 40px rgba(15, 23, 42, 0.35)); +} + +.hero-cta { + margin-top: 0.5rem; +} + +.section-title { + margin: 0; + text-align: center; + font-size: clamp(1.4rem, 3.4vw, 2rem); + font-weight: 800; + letter-spacing: -0.05em; + color: var(--app-text-strong); +} + +.section-intro { + max-width: 820px; + margin: 0; + text-align: center; + color: var(--app-text-muted); +} + +.tile-grid, +.dashboard-grid { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; + align-items: stretch; +} + +.feature-card { + min-height: 230px; + justify-content: center; + border: 1px solid var(--app-border-strong); + border-radius: 24px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(245, 249, 255, 0.88)); + box-shadow: var(--app-shadow-md); + transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease; +} + +.feature-card:hover, +.message-card:hover { + transform: translateY(-4px); + box-shadow: 0 24px 48px rgba(15, 23, 42, 0.14); + border-color: rgba(37, 99, 235, 0.24); +} + +.feature-card-icon, +.login-highlight-icon { + color: var(--app-accent-strong); +} + +.feature-card-icon { + padding: 0.95rem; + border-radius: 22px; + background: linear-gradient(180deg, rgba(37, 99, 235, 0.12), rgba(14, 165, 233, 0.18)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55); +} + +.feature-card-title { + margin: 0; + text-align: center; + font-size: 1.1rem; + font-weight: 700; + color: var(--app-text-strong); +} + +.feature-card-description { + margin: 0; + text-align: center; + color: var(--app-text-muted); +} + +.footer-details p, +.footer-cta, +.footer-slogan, +.footer-version { + margin: 0; + text-align: center; +} + +.footer-cta { + margin-top: 0.75rem; + color: var(--app-accent-strong); + font-weight: 700; +} + +.footer-slogan { + color: var(--app-accent-strong); + font-style: italic; +} + +.footer-version { + color: var(--app-text-muted); + font-size: var(--lumo-font-size-s); +} + +.login-view { + padding-inline: 0; +} + +.login-shell { + width: min(1120px, 100%); + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(360px, 440px); + gap: 1.5rem; +} + +.login-highlight, +.login-card { + border-radius: 32px; +} + +.login-highlight { + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + gap: 1rem; + min-height: 560px; + padding: clamp(1.8rem, 4vw, 3rem); + border: 1px solid rgba(255, 255, 255, 0.14); + background: linear-gradient(140deg, #081224 0%, #173d8d 52%, #0f766e 100%); + box-shadow: var(--app-shadow-xl); +} + +.eyebrow-chip { + display: inline-flex; + align-items: center; + width: fit-content; + padding: 0.45rem 0.8rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.14); + color: rgba(248, 250, 252, 0.92); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.login-highlight-icon { + width: 78px; + height: 78px; + padding: 1rem; + border-radius: 22px; + color: #f8fafc; + background: rgba(255, 255, 255, 0.12); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24); +} + +.login-card { + width: 100%; + padding: clamp(1.35rem, 3vw, 2rem); + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.88); + backdrop-filter: blur(20px); + box-shadow: var(--app-shadow-lg); +} + +.login-card-title { + margin: 0; + color: var(--app-text-strong); + font-size: clamp(1.8rem, 4vw, 2.4rem); + font-weight: 800; + letter-spacing: -0.06em; +} + +.login-register-button { + margin-top: 0.25rem; +} + +.login-version { + color: var(--app-text-muted); + font-size: var(--lumo-font-size-s); +} + +.inline-flash { + width: 100%; + padding: 0.95rem 1rem; + border-radius: 18px; + border: 1px solid rgba(220, 38, 38, 0.22); + background: rgba(254, 242, 242, 0.92); + color: var(--lumo-error-text-color); + box-sizing: border-box; +} + +.app-drawer-header { + margin: 0.65rem 0.5rem 0.8rem; + padding: 0.9rem 1rem; + border-radius: 28px; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.28), rgba(20, 184, 166, 0.2)); + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 18px 40px rgba(8, 18, 36, 0.24); +} + +.app-drawer-logo-icon { + color: #f8fafc; + padding: 0.8rem; + border-radius: 20px; + background: rgba(255, 255, 255, 0.14); +} + +.app-drawer-title { + color: #f8fafc; + font-size: 1.1rem; + letter-spacing: -0.04em; +} + +.app-drawer-scroll { + flex: 1 1 auto; + min-height: 0; + padding: 0 0.35rem 0.4rem; + box-sizing: border-box; + overflow: hidden; +} + +.app-drawer-scroll::part(content) { + overflow: hidden auto; +} + +.app-nav-container { + width: 100%; + min-width: 0; +} + +.app-nav-tree { + border: 0; + background: transparent; + min-width: 0; + overflow: hidden; + border-radius: 0 !important; + box-shadow: none !important; +} + +.app-nav-tree::part(scroller), +.app-nav-tree::part(table), +.app-nav-tree::part(rows) { + background: transparent; + box-shadow: none; +} + +.app-nav-tree::part(header-cell), +.app-nav-tree::part(first-header-row-cell) { + min-height: 0; + height: 0; + padding: 0; + border: 0; + background: transparent; +} + +.app-nav-tree::part(body-cell), +.app-nav-tree::part(selected-row-cell) { + background: transparent; + border: 0; + padding: 0.15rem 0.35rem; + box-sizing: border-box; + overflow: hidden; +} + +.app-nav-row { + width: auto; + max-width: 100%; + min-width: 0; + box-sizing: border-box; + margin: 0.1rem 0; + padding: 0.72rem 0.85rem; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.08); + transition: transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease; +} + +.app-nav-row:hover { + transform: translateX(4px); + background: rgba(255, 255, 255, 0.14); + border-color: rgba(255, 255, 255, 0.16); +} + +.app-nav-icon { + color: #93c5fd; +} + +.app-nav-label { + min-width: 0; + overflow-wrap: break-word; + word-break: break-word; + color: #eff6ff; + font-weight: 600; +} + +/* Chevron for expandable parent nodes */ +.nav-expand-icon { + color: rgba(147, 197, 253, 0.55); + flex-shrink: 0; + margin-left: auto; + transition: transform 0.2s ease, color 0.18s ease; +} + +/* Hide on leaf nodes (no children) */ +vaadin-grid-tree-toggle[leaf] .nav-expand-icon { + display: none; +} + +/* Rotate 90° when the parent is expanded */ +vaadin-grid-tree-toggle[expanded] .nav-expand-icon { + transform: rotate(90deg); + color: rgba(147, 197, 253, 0.9); +} + +.app-nav-badge { + margin-left: auto; + padding: 0.2rem 0.58rem; + min-width: 1.55rem; + border-radius: 999px; + background: linear-gradient(135deg, #60a5fa, #2563eb); + color: #f8fafc; + font-size: 0.72rem; + font-weight: 700; + text-align: center; + box-shadow: 0 12px 26px rgba(37, 99, 235, 0.3); +} + +.app-side-nav { + width: 100%; + background: transparent; + min-width: 0; +} + +.app-side-nav vaadin-side-nav-item { + margin-bottom: 0.2rem; +} + +.app-side-nav vaadin-side-nav-item::part(content) { + border-radius: 18px; + padding: 0.72rem 0.85rem; + color: #eff6ff; +} + +.app-side-nav vaadin-side-nav-item:hover::part(content), +.app-side-nav vaadin-side-nav-item[current]::part(content) { + background: rgba(255, 255, 255, 0.14); + transform: translateX(4px); +} + +.app-user-menu { + margin: auto 0.5rem 0.5rem; + padding: 0.45rem 0.65rem; + border-radius: 22px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(14px); + overflow: visible; +} + +@media (max-height: 820px) { + .app-drawer-header { + margin-bottom: 0.6rem; + padding: 0.8rem 0.9rem; + } + + .app-nav-row, + .app-side-nav vaadin-side-nav-item::part(content) { + padding: 0.62rem 0.78rem; + } + + .app-user-menu { + margin-bottom: 0.35rem; + } +} + +.app-user-menu vaadin-menu-bar-button { + color: #f8fafc; + border-radius: 16px; + overflow: visible; +} + +.app-user-menu vaadin-menu-bar-button::part(button) { + padding: 0.25rem 0.75rem 0.25rem 0.95rem; + overflow: visible; +} + +.app-user-name { + color: #f8fafc; + font-weight: 700; +} + +.app-user-avatar { + margin-inline-start: 0.1rem; + flex-shrink: 0; + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.14); +} + +.view-toolbar { + width: 100%; + padding: 0.75rem 1rem; + box-sizing: border-box; +} + +.view-toolbar-title-row { + gap: 0.8rem; +} + +.view-toolbar-title { + color: var(--app-text-strong); + font-weight: 800; + letter-spacing: -0.05em; +} + +.view-toolbar-actions, +.view-toolbar-group { + width: 100%; + justify-content: flex-end; +} + +.filter-panel { + padding: 1rem 1.15rem; + border-radius: 26px; + flex-wrap: wrap; + gap: 1rem; + min-width: 0; + max-width: 100%; + box-sizing: border-box; +} + +.filter-panel > * { + flex-wrap: wrap; + min-width: 0; +} + +.data-grid-panel { + padding: 0.8rem; + min-height: 420px; + overflow: hidden; + box-sizing: border-box; + max-width: 100%; +} + +.data-grid-panel > vaadin-grid { + height: 100%; +} + +.dashboard-title { + margin: 0 !important; + font-size: clamp(1.7rem, 3.2vw, 2.5rem); + font-weight: 800; + letter-spacing: -0.06em; +} + +.dashboard-sections { + gap: 1rem; +} + +.dashboard-section-title { + margin: 0; + font-size: 1.12rem; + font-weight: 700; + letter-spacing: -0.03em; +} + +.dashboard-stat-card { + position: relative; + overflow: hidden; + border-radius: 24px; + padding: 1.15rem; + border: 1px solid var(--app-border-strong); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.92)); + box-shadow: var(--app-shadow-md); +} + +.dashboard-stat-card::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 4px; + background: var(--dashboard-accent, var(--app-accent)); +} + +.accent-blue { + --dashboard-accent: #2563eb; +} + +.accent-green { + --dashboard-accent: #059669; +} + +.accent-purple { + --dashboard-accent: #7c3aed; +} + +.accent-orange { + --dashboard-accent: #d97706; +} + +.accent-gray { + --dashboard-accent: #64748b; +} + +.accent-red { + --dashboard-accent: #dc2626; +} + +.dashboard-stat-card-header { + gap: 0.75rem; +} + +.dashboard-stat-icon { + color: var(--dashboard-accent, var(--app-accent)); +} + +.dashboard-stat-title { + color: var(--app-text-muted); + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.dashboard-stat-value { + color: var(--app-text-strong); + line-height: 1.2; +} + +.dashboard-stat-content { + gap: 0.65rem; +} + +.station-tile, +.delivery-station-card, +.add-station-tile { + border-radius: 24px !important; + border: 1px solid var(--app-border-strong) !important; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 248, 255, 0.9)) !important; + box-shadow: var(--app-shadow-sm); +} + +.station-tile { + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.station-tile:hover, +.add-station-tile:hover { + transform: translateY(-3px); + box-shadow: var(--app-shadow-md); + border-color: rgba(37, 99, 235, 0.24) !important; +} + +.add-station-tile { + background: linear-gradient(180deg, rgba(241, 245, 249, 0.9), rgba(248, 250, 252, 0.98)) !important; + border-style: dashed !important; +} + +.stations-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; + width: 100%; + align-items: start; +} + +.station-slot { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; +} + +.station-slot > .station-slot-content { + width: 100%; + max-width: min(100%, 26rem); + box-sizing: border-box; +} + +.route-card, +.summary-card, +.services-panel, +.notes-panel { + border: 1px solid var(--app-border-strong); + border-radius: 24px; + background: rgba(255, 255, 255, 0.84); + box-shadow: var(--app-shadow-sm); +} + +.route-card, +.summary-card { + padding: 1rem; +} + +.services-panel, +.notes-panel { + padding: 0.75rem 1rem 1rem; +} + +.detail-card, +.dialog-form-panel, +.dialog-content-panel, +.dialog-task-card, +.dialog-cargo-card, +.invoice-generator-panel, +.invoice-section-card, +.invoice-summary-card, +.timeline-entry-card, +.job-summary-map, +.job-photo-gallery { + border: 1px solid var(--app-border-strong); + border-radius: 24px; + background: rgba(255, 255, 255, 0.9); + box-shadow: var(--app-shadow-sm); +} + +.detail-card, +.dialog-form-panel, +.dialog-content-panel, +.dialog-cargo-card, +.invoice-generator-panel, +.invoice-section-card, +.invoice-summary-card, +.timeline-entry-card { + padding: 1rem; +} + +.detail-card--accent { + border-color: rgba(37, 99, 235, 0.22); + background: linear-gradient(180deg, rgba(219, 234, 254, 0.62), rgba(239, 246, 255, 0.9)); +} + +.detail-card--asset { + display: flex; + align-items: center; + justify-content: center; + background: rgba(248, 250, 252, 0.96); +} + +.detail-card--code, +.detail-card--comment { + font-family: ui-monospace, "SFMono-Regular", "SF Mono", Menlo, Consolas, monospace; +} + +.detail-card--code { + background: rgba(248, 250, 252, 0.96); + word-break: break-all; +} + +.detail-card--comment { + background: rgba(248, 250, 252, 0.92); + white-space: pre-wrap; +} + +.dialog-form-panel, +.dialog-content-panel, +.dialog-cargo-card { + backdrop-filter: blur(12px); +} + +.dialog-task-card { + position: relative; + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.dialog-task-card:hover { + transform: translateY(-2px); + box-shadow: var(--app-shadow-md); + border-color: rgba(37, 99, 235, 0.24); +} + +.dialog-floating-delete { + position: absolute; + top: 0.65rem; + right: 0.65rem; + z-index: 10; + padding: 0.2rem; + min-width: 1.7rem; + min-height: 1.7rem; +} + +.inline-caption, +.message-subtitle, +.invoice-generator-info { + color: var(--app-text-muted); + font-size: 0.85rem; +} + +.invoice-top-grid { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1rem; +} + +.invoice-card-inner { + padding-top: 0.5rem; +} + +.empty-state-card { + text-align: center; + color: var(--app-text-muted); + padding: 1rem; + border: 1px dashed var(--app-border-strong); + border-radius: 24px; + background: rgba(248, 250, 252, 0.74); +} + +.timeline-entry-card { + margin-bottom: 0.75rem; + border-left: 4px solid var(--timeline-accent, var(--app-accent)); +} + +.timeline-reason { + font-weight: 700; + color: var(--app-text-strong); +} + +.timeline-timestamp { + color: var(--app-text-muted); + font-size: 0.78rem; +} + +.timeline-description { + display: block; + margin-top: 0.35rem; + color: var(--app-text); +} + +.price-table { + width: 100%; +} + +.price-row { + display: flex; + justify-content: space-between; + gap: 1rem; + padding: 0.35rem 0; +} + +.price-row-label { + padding-right: 0.5rem; + color: var(--app-text-muted); +} + +.price-row-value { + white-space: nowrap; + color: var(--app-text-strong); +} + +.price-row--strong .price-row-label, +.price-row--strong .price-row-value { + font-weight: 800; + color: var(--app-text-strong); +} + +.message-card { + padding: 1rem !important; + border: 1px solid var(--app-border-strong) !important; + border-radius: 22px !important; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.9)) !important; + box-shadow: var(--app-shadow-sm) !important; + max-width: 100% !important; + margin-bottom: 0.85rem !important; +} + +.message-section { + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.88); + box-shadow: var(--app-shadow-md); + border-radius: 28px; +} + +.message-section-title { + margin: 0; + font-size: 1.05rem; + font-weight: 800; + color: var(--app-text-strong); +} + +.message-preview { + color: var(--app-text-muted); + font-size: 0.9rem; +} + +.message-meta { + gap: 0.75rem; + color: var(--app-text-muted); + font-size: 0.78rem; +} + +.message-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.65rem; + height: 1.65rem; + padding: 0 0.45rem; + border-radius: 999px; + background: linear-gradient(135deg, #2563eb, #1d4ed8); + color: #eff6ff; + font-size: 0.76rem; + font-weight: 800; + box-shadow: 0 12px 24px rgba(37, 99, 235, 0.24); +} + +.message-thread-layout { + width: min(1240px, 100%); + margin: 0 auto; + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.84); + box-shadow: var(--app-shadow-lg); + border-radius: 30px; +} + +.message-thread-header { + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.78); + box-shadow: var(--app-shadow-sm); + border-radius: 22px; + padding: 1rem 1.15rem; +} + +.message-thread { + border: 1px solid var(--app-border-strong); + background: linear-gradient(180deg, rgba(239, 244, 255, 0.72), rgba(248, 250, 252, 0.92)); + border-radius: 24px; + padding: 1rem !important; +} + +.message-thread-input { + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.82); + border-radius: 22px; + padding: 0.85rem 1rem; +} + +.message-date-separator { + display: flex; + justify-content: center; + text-align: center; + margin: 1rem 0; + width: 100%; +} + +.message-date-chip { + display: inline-flex; + align-items: center; + padding: 0.35rem 0.8rem; + border-radius: 999px; + background: rgba(203, 213, 225, 0.72); + color: var(--app-text); + font-size: 0.75rem; + font-weight: 700; +} + +.message-wrapper { + display: flex; + width: 100%; + margin: 0.35rem 0; +} + +.message-wrapper.client { + justify-content: flex-start; +} + +.message-wrapper.server { + justify-content: flex-end; +} + +.message-bubble { + max-width: min(78%, 720px); + padding: 0.85rem 1rem; + border-radius: 22px; + box-shadow: var(--app-shadow-sm); + word-wrap: break-word; + white-space: pre-wrap; +} + +.message-bubble.client { + background: rgba(255, 255, 255, 0.96); + border: 1px solid var(--app-border-strong); +} + +.message-bubble.server { + background: linear-gradient(135deg, rgba(191, 219, 254, 0.8), rgba(167, 243, 208, 0.74)); + border: 1px solid rgba(59, 130, 246, 0.22); +} + +.message-bubble-text { + margin-bottom: 0.35rem; + color: var(--app-text-strong); + font-size: 0.92rem; +} + +.message-bubble-image { + max-width: 100%; + max-height: 320px; + height: auto; + display: block; + border-radius: 16px; +} + +.message-bubble-time { + display: block; + font-size: 0.72rem; + color: var(--app-text-muted); + opacity: 0.9; +} + +.message-image-row { + margin-bottom: 0.35rem; + display: flex; +} + +.message-image-row.client { + justify-content: flex-start; +} + +.message-image-row.server { + justify-content: flex-end; +} + +.statistics-header, +.statistics-input-panel { + width: min(1240px, 100%); + margin: 0 auto; + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.86); + box-shadow: var(--app-shadow-sm); + border-radius: 26px; +} + +.statistics-scroller { + width: min(1240px, 100%); + margin: 0 auto; + border: 1px solid var(--app-border-strong); + background: linear-gradient(180deg, rgba(238, 244, 255, 0.75), rgba(248, 250, 252, 0.96)); + border-radius: 30px; + box-shadow: var(--app-shadow-lg); +} + +.station-tile.validated { + background: linear-gradient(180deg, rgba(236, 253, 245, 0.98), rgba(209, 250, 229, 0.88)) !important; + border-color: rgba(5, 150, 105, 0.26) !important; +} + +.job-summary-map { + min-height: 520px; +} + +.job-task-card { + display: flex; + align-items: center; + gap: 1rem; + width: 100%; + box-sizing: border-box; + cursor: pointer; + margin: 0.25rem 0; + padding: 1rem; + border: 1px solid var(--app-border-strong); + border-radius: 22px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.9)); + box-shadow: var(--app-shadow-sm); + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.job-task-card:hover { + transform: translateY(-2px); + box-shadow: var(--app-shadow-md); + border-color: rgba(37, 99, 235, 0.24); +} + +.job-task-card.completed { + border-color: rgba(5, 150, 105, 0.22); + background: linear-gradient(180deg, rgba(236, 253, 245, 0.98), rgba(220, 252, 231, 0.88)); +} + +.job-task-icon { + color: var(--app-accent); +} + +.job-task-card.completed .job-task-icon { + color: var(--lumo-success-color); +} + +.job-task-content { + flex-grow: 1; + gap: 0.15rem; +} + +.job-task-name { + font-size: 1rem; + font-weight: 700; + color: var(--app-text-strong); +} + +.job-task-card.completed .job-task-name { + color: var(--lumo-success-text-color); +} + +.job-task-status { + padding: 0.2rem 0.55rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 800; +} + +.job-task-status.open { + background: rgba(220, 38, 38, 0.12); + color: var(--lumo-error-text-color); +} + +.job-task-status.completed { + background: rgba(5, 150, 105, 0.12); + color: var(--lumo-success-text-color); +} + +.job-task-description { + margin-top: 0.2rem; + font-size: 0.86rem; + color: var(--app-text-muted); +} + +.job-task-indicator { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--lumo-error-color); +} + +.job-task-card.completed .job-task-indicator { + background: var(--lumo-success-color); +} + +.job-photo-gallery { + max-width: 600px; + min-height: 500px; + height: 500px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(255, 255, 255, 0.96); +} + +.job-photo-container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + flex: 1; +} + +.job-photo-counter { + position: absolute; + top: var(--lumo-space-s); + right: var(--lumo-space-s); + z-index: 10; + padding: var(--lumo-space-xs) var(--lumo-space-s); + border-radius: 999px; + background: rgba(15, 23, 42, 0.72); + color: #f8fafc; + font-size: var(--lumo-font-size-s); + font-weight: 700; +} + +.job-photo-nav { + position: absolute; + top: 50%; + z-index: 10; + transform: translateY(-50%); + border-radius: 999px; + background: rgba(255, 255, 255, 0.9); + box-shadow: var(--app-shadow-sm); +} + +.job-photo-nav.prev { + left: var(--lumo-space-s); +} + +.job-photo-nav.next { + right: var(--lumo-space-s); +} + +.invoice-generator-view { + gap: 1rem; +} + +.invoice-generator-main { + gap: 1rem; +} + +.invoice-generator-panel { + backdrop-filter: blur(16px); +} + +.invoice-generator-panel-title { + color: var(--app-text-strong); + font-size: 1rem; + font-weight: 800; +} + +.invoice-generator-template { + display: flex; + align-items: center; + gap: 0.75rem; + margin: 0.25rem 0; + padding: 0.95rem 1rem; + border: 1px solid var(--app-border-strong); + border-radius: 18px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(246, 249, 255, 0.92)); + cursor: grab; + user-select: none; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; +} + +.invoice-generator-template:hover { + transform: translateY(-2px); + box-shadow: var(--app-shadow-sm); + border-color: rgba(37, 99, 235, 0.24); +} + +.invoice-generator-canvas { + position: relative; + overflow: hidden; + cursor: default; + border: 2px dashed rgba(148, 163, 184, 0.5); + border-radius: 24px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(241, 245, 249, 0.9)), + linear-gradient(90deg, rgba(148, 163, 184, 0.08) 1px, transparent 1px), + linear-gradient(rgba(148, 163, 184, 0.08) 1px, transparent 1px); + background-size: auto, 28px 28px, 28px 28px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84); +} + +.invoice-generator-actionbar { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--app-border-strong); + border-radius: 24px; + background: rgba(255, 255, 255, 0.84); + box-shadow: var(--app-shadow-sm); +} + +.invoice-generator-pdf { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.invoice-generator-frame { + border: none; + flex-grow: 1; +} + +.statistics-chat-container { + padding-bottom: 20px; +} + +.statistics-quick-actions { + flex-wrap: wrap; +} + +.chat-message { + display: flex; + width: 100%; + margin-bottom: var(--lumo-space-m); +} + +.chat-message.user-message { + justify-content: flex-end; +} + +.chat-message.ai-message, +.chat-message.loading-message, +.chat-message.error-message { + justify-content: flex-start; +} + +.chat-bubble { + max-width: min(86%, 760px); + border-radius: 24px; + box-shadow: var(--app-shadow-sm); +} + +.chat-bubble--user { + background: linear-gradient(135deg, #2563eb, #1d4ed8); + color: #eff6ff; + padding: 0.85rem 1rem; +} + +.chat-bubble--ai, +.chat-bubble--loading, +.chat-bubble--error { + background: rgba(255, 255, 255, 0.94); + border: 1px solid var(--app-border-strong); + padding: 1rem; +} + +.chat-bubble--error { + border-color: rgba(220, 38, 38, 0.18); + background: rgba(254, 242, 242, 0.92); +} + +.chart-wrapper { + background: rgba(241, 245, 249, 0.68) !important; + border-radius: 22px !important; + padding: 0.85rem !important; + border: 1px solid var(--app-border-strong); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.76); +} + +vaadin-button { + font-weight: 700; + transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease; +} + +vaadin-button:hover { + transform: translateY(-1px); +} + +vaadin-button[theme~="primary"] { + box-shadow: 0 16px 28px rgba(37, 99, 235, 0.22); +} + +vaadin-text-field, +vaadin-text-area, +vaadin-combo-box, +vaadin-date-picker, +vaadin-password-field, +vaadin-email-field, +vaadin-number-field, +vaadin-time-picker, +vaadin-select, +vaadin-multi-select-combo-box { + --vaadin-input-field-background: #ffffff; + --vaadin-input-field-border-width: 1px; + --vaadin-input-field-border-color: var(--app-field-border); + --vaadin-input-field-border-radius: 20px; + --vaadin-input-field-hover-highlight: rgba(37, 99, 235, 0.1); + --vaadin-input-field-focus-ring-color: rgba(37, 99, 235, 0.18); +} + +vaadin-text-field::part(input-field), +vaadin-text-area::part(input-field), +vaadin-combo-box::part(input-field), +vaadin-date-picker::part(input-field), +vaadin-password-field::part(input-field), +vaadin-email-field::part(input-field), +vaadin-number-field::part(input-field), +vaadin-time-picker::part(input-field), +vaadin-select::part(input-field), +vaadin-multi-select-combo-box::part(input-field) { + border: 1px solid var(--app-field-border); + border-radius: 20px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82); + transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; +} + +vaadin-text-field:hover::part(input-field), +vaadin-text-area:hover::part(input-field), +vaadin-combo-box:hover::part(input-field), +vaadin-date-picker:hover::part(input-field), +vaadin-password-field:hover::part(input-field), +vaadin-email-field:hover::part(input-field), +vaadin-number-field:hover::part(input-field), +vaadin-time-picker:hover::part(input-field), +vaadin-select:hover::part(input-field), +vaadin-multi-select-combo-box:hover::part(input-field) { + border-color: var(--app-field-border-hover); +} + +vaadin-grid, +vaadin-grid-pro { + border: 0; + border-radius: 24px; + overflow: hidden; + background: rgba(255, 255, 255, 0.88); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +vaadin-grid::part(header-cell), +vaadin-grid-pro::part(header-cell) { + background: rgba(226, 232, 240, 0.55); + color: var(--app-text-muted); + font-size: 0.76rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + border-bottom: 1px solid var(--app-border); +} + +vaadin-grid::part(body-cell), +vaadin-grid-pro::part(body-cell) { + background: rgba(255, 255, 255, 0.82); + border-bottom: 1px solid rgba(226, 232, 240, 0.82); +} + +vaadin-grid::part(selected-row-cell), +vaadin-grid-pro::part(selected-row-cell) { + background: rgba(37, 99, 235, 0.08); +} + +vaadin-dialog-overlay::part(content), +vaadin-confirm-dialog-overlay::part(content) { + border-radius: 28px; + border: 1px solid var(--app-border-strong); + background: rgba(255, 255, 255, 0.94); + backdrop-filter: blur(20px); + box-shadow: var(--app-shadow-xl); +} + +vaadin-tabsheet::part(content) { + border-radius: 0 0 24px 24px; + background: rgba(255, 255, 255, 0.86); +} + +vaadin-notification-card { + border-radius: 20px; + border: 1px solid var(--app-border-strong); + box-shadow: var(--app-shadow-lg); +} + +.message-card, +.chat-message, +.chart-wrapper { + border: 1px solid var(--app-border-strong); + border-radius: 22px; + box-shadow: var(--app-shadow-sm); +} + +@media (max-width: 980px) { + .login-shell { + grid-template-columns: 1fr; + } + + .login-highlight { + min-height: 340px; + } +} + +@media (max-width: 720px) { + .view-container { + margin: 10px; + width: calc(100% - 20px); + max-width: calc(100% - 20px); + height: calc(100dvh - 20px); + border-radius: 8px; + } + + .surface-panel, + .hero-panel, + .login-card, + .login-highlight { + border-radius: 24px; + } +} diff --git a/src/main/frontend/themes/votian-modern/theme.json b/src/main/frontend/themes/votian-modern/theme.json new file mode 100644 index 0000000..2a6e038 --- /dev/null +++ b/src/main/frontend/themes/votian-modern/theme.json @@ -0,0 +1,9 @@ +{ + "lumoImports": [ + "typography", + "color", + "spacing", + "badge", + "utility" + ] +} diff --git a/src/main/java/de/assecutor/votianlt/Application.java b/src/main/java/de/assecutor/votianlt/Application.java index 93e33da..36ef018 100644 --- a/src/main/java/de/assecutor/votianlt/Application.java +++ b/src/main/java/de/assecutor/votianlt/Application.java @@ -14,7 +14,7 @@ import java.time.Clock; @SpringBootApplication @EnableAsync @EnableScheduling -@Theme("default") +@Theme("votian-modern") @Push public class Application implements AppShellConfigurator { diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java index 4b8d97c..c4d79d1 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationDialog.java @@ -216,6 +216,7 @@ public class DeliveryStationDialog extends Dialog { formLayout.setPadding(true); formLayout.setSpacing(true); formLayout.setWidthFull(); + formLayout.addClassName("dialog-form-panel"); // Company with autocomplete company = new ComboBox<>(translationHelper.getTranslation("profile.company")); @@ -694,6 +695,7 @@ public class DeliveryStationDialog extends Dialog { content.setPadding(false); content.setSpacing(true); content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); + content.addClassName("dialog-content-panel"); // Task title with template selection H3 tasksTitle = new H3(translationHelper.getTranslation("addjob.tasks.title")); @@ -749,10 +751,7 @@ public class DeliveryStationDialog extends Dialog { VerticalLayout taskContainer = new VerticalLayout(); taskContainer.setPadding(true); taskContainer.setSpacing(true); - taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); - taskContainer.getStyle().set("position", "relative"); + taskContainer.addClassName("dialog-task-card"); // Task type selection ComboBox taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype")); @@ -769,13 +768,7 @@ public class DeliveryStationDialog extends Dialog { // Red X button positioned in top-right corner Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - deleteXButton.getStyle().set("position", "absolute"); - deleteXButton.getStyle().set("top", "8px"); - deleteXButton.getStyle().set("right", "8px"); - deleteXButton.getStyle().set("z-index", "10"); - deleteXButton.getStyle().set("padding", "4px"); - deleteXButton.getStyle().set("min-width", "24px"); - deleteXButton.getStyle().set("min-height", "24px"); + deleteXButton.addClassName("dialog-floating-delete"); deleteXButton.addClickListener(e -> { int idx = tasksList.getChildren().toList().indexOf(taskContainer); if (idx >= 0 && idx < tasksState.size()) { @@ -852,10 +845,7 @@ public class DeliveryStationDialog extends Dialog { VerticalLayout taskContainer = new VerticalLayout(); taskContainer.setPadding(true); taskContainer.setSpacing(true); - taskContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - taskContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - taskContainer.getStyle().set("background-color", "var(--lumo-base-color)"); - taskContainer.getStyle().set("position", "relative"); + taskContainer.addClassName("dialog-task-card"); ComboBox taskTypeCombo = new ComboBox<>(translationHelper.getTranslation("addjob.tasks.tasktype")); taskTypeCombo.setItems(TaskType.values()); @@ -869,13 +859,7 @@ public class DeliveryStationDialog extends Dialog { Button deleteXButton = new Button(new Icon(VaadinIcon.CLOSE_SMALL)); deleteXButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY); - deleteXButton.getStyle().set("position", "absolute"); - deleteXButton.getStyle().set("top", "8px"); - deleteXButton.getStyle().set("right", "8px"); - deleteXButton.getStyle().set("z-index", "10"); - deleteXButton.getStyle().set("padding", "4px"); - deleteXButton.getStyle().set("min-width", "24px"); - deleteXButton.getStyle().set("min-height", "24px"); + deleteXButton.addClassName("dialog-floating-delete"); deleteXButton.addClickListener(e -> { int idx = tasksList.getChildren().toList().indexOf(taskContainer); if (idx >= 0 && idx < tasksState.size()) { diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java index 9abd8b0..cd147a1 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/DeliveryStationTile.java @@ -67,12 +67,10 @@ public class DeliveryStationTile extends VerticalLayout { setSpacing(true); setPadding(true); - setWidth("40%"); - getStyle().set("min-width", "300px"); - getStyle().set("flex-shrink", "0"); - getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - getStyle().set("background-color", "var(--lumo-base-color)"); + setWidthFull(); + addClassName("delivery-station-card"); + getStyle().set("min-width", "0"); + getStyle().set("box-sizing", "border-box"); // Header with title, collapse button and delete button on one line title = new H3(translationHelper.getTranslation("addjob.station.delivery", stationNumber)); @@ -150,8 +148,9 @@ public class DeliveryStationTile extends VerticalLayout { HorizontalLayout streetLayout = new HorizontalLayout(); streetLayout.setWidthFull(); streetLayout.setSpacing(true); - street.setWidth("70%"); - houseNumber.setWidth("30%"); + streetLayout.getStyle().set("flex-wrap", "wrap"); + street.getStyle().set("flex", "3 1 150px").set("min-width", "0"); + houseNumber.getStyle().set("flex", "1 1 60px").set("min-width", "0"); streetLayout.add(street, houseNumber); add(streetLayout); @@ -174,8 +173,9 @@ public class DeliveryStationTile extends VerticalLayout { HorizontalLayout zipCityLayout = new HorizontalLayout(); zipCityLayout.setWidthFull(); zipCityLayout.setSpacing(true); - zip.setWidth("30%"); - city.setWidth("70%"); + zipCityLayout.getStyle().set("flex-wrap", "wrap"); + zip.getStyle().set("flex", "1 1 80px").set("min-width", "0"); + city.getStyle().set("flex", "3 1 150px").set("min-width", "0"); zipCityLayout.add(zip, city); add(zipCityLayout); @@ -192,7 +192,7 @@ public class DeliveryStationTile extends VerticalLayout { // stays visible) expandedOnlyComponents = getChildren().filter(c -> c != titleLayout).toList(); - getStyle().set("transition", "width 0.3s ease, min-width 0.3s ease"); + getStyle().set("transition", "opacity 0.2s ease"); // Collapsed content (initially hidden) collapsedContent = new VerticalLayout(); @@ -446,14 +446,10 @@ public class DeliveryStationTile extends VerticalLayout { updateCollapsedContent(); expandedOnlyComponents.forEach(c -> c.setVisible(false)); collapsedContent.setVisible(true); - setWidth("25%"); - getStyle().set("min-width", "150px"); collapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_RIGHT)); } else { expandedOnlyComponents.forEach(c -> c.setVisible(true)); collapsedContent.setVisible(false); - setWidth("40%"); - getStyle().set("min-width", "300px"); collapseButton.setIcon(new Icon(VaadinIcon.ANGLE_DOUBLE_LEFT)); } if (collapseListener != null) { diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java index da01d8a..c39443d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/PickupStationDialog.java @@ -267,6 +267,7 @@ public class PickupStationDialog extends Dialog { formLayout.setPadding(true); formLayout.setSpacing(true); formLayout.setWidthFull(); + formLayout.addClassName("dialog-form-panel"); // Customer selection customerComboBox = new ComboBox<>(translationHelper.getTranslation("addjob.customer.label")); @@ -760,6 +761,7 @@ public class PickupStationDialog extends Dialog { content.setSpacing(true); content.setWidth("720px"); content.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); + content.addClassName("dialog-content-panel"); // Digital processing + App user digitalProcessingCheckbox = new Checkbox(translationHelper.getTranslation("profile.settings.digitalprocess")); @@ -827,8 +829,7 @@ public class PickupStationDialog extends Dialog { // Info about delivery dates Span deliveryInfoLabel = new Span(translationHelper.getTranslation("addjob.appointment.delivery.info")); - deliveryInfoLabel.getStyle().set("color", "var(--lumo-secondary-text-color)"); - deliveryInfoLabel.getStyle().set("font-style", "italic"); + deliveryInfoLabel.addClassName("inline-caption"); deliveryInfoLabel.getStyle().set("margin-top", "var(--lumo-space-m)"); content.add(deliveryInfoLabel); @@ -853,10 +854,7 @@ public class PickupStationDialog extends Dialog { VerticalLayout cargoAreaContainer = new VerticalLayout(); cargoAreaContainer.setWidthFull(); cargoAreaContainer.setSpacing(true); - cargoAreaContainer.getStyle().set("background", "var(--lumo-base-color)"); - cargoAreaContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - cargoAreaContainer.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - cargoAreaContainer.getStyle().set("padding", "var(--lumo-space-m)"); + cargoAreaContainer.addClassName("dialog-cargo-card"); H3 cargoTitle = new H3(translationHelper.getTranslation("addjob.tab.cargo")); diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java index 263fad1..a67a843 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/StationTile.java @@ -43,9 +43,7 @@ public class StationTile extends VerticalLayout { setPadding(false); setSpacing(false); - getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - getStyle().set("background-color", "var(--lumo-base-color)"); + addClassName("station-tile"); getStyle().set("cursor", "pointer"); getStyle().set("aspect-ratio", "1 / 1"); getStyle().set("overflow", "hidden"); @@ -196,11 +194,9 @@ public class StationTile extends VerticalLayout { public void setAddressValidated(boolean validated) { if (validated) { - getStyle().set("background-color", "rgba(76, 175, 80, 0.15)"); - getStyle().set("border", "1px solid rgba(76, 175, 80, 0.3)"); + addClassName("validated"); } else { - getStyle().set("background-color", "var(--lumo-base-color)"); - getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); + removeClassName("validated"); } } } diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/ViewToolbar.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/ViewToolbar.java index 55dafba..cb56736 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/component/ViewToolbar.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/component/ViewToolbar.java @@ -15,6 +15,8 @@ public final class ViewToolbar extends Composite
{ } public ViewToolbar(String viewTitle, boolean showDrawerToggle, Component... components) { + addClassName("view-toolbar-host"); + getContent().addClassName("view-toolbar"); addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.BETWEEN, AlignItems.STRETCH, Gap.MEDIUM, FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER); @@ -22,21 +24,26 @@ public final class ViewToolbar extends Composite
{ if (showDrawerToggle) { var drawerToggle = new DrawerToggle(); drawerToggle.addClassNames(Margin.NONE); + drawerToggle.addClassName("view-toolbar-toggle"); var title = new H1(viewTitle); title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT); + title.addClassName("view-toolbar-title"); toggleAndTitle = new Div(drawerToggle, title); } else { var title = new H1(viewTitle); title.addClassNames(FontSize.XLARGE, Margin.NONE, FontWeight.LIGHT); + title.addClassName("view-toolbar-title"); toggleAndTitle = new Div(title); } toggleAndTitle.addClassNames(Display.FLEX, AlignItems.CENTER); + toggleAndTitle.addClassName("view-toolbar-title-row"); getContent().add(toggleAndTitle); if (components.length > 0) { var actions = new Div(components); actions.addClassNames(Display.FLEX, FlexDirection.COLUMN, JustifyContent.END, Flex.GROW, Gap.SMALL, FlexDirection.Breakpoint.Medium.ROW); + actions.addClassName("view-toolbar-actions"); getContent().add(actions); } } @@ -45,6 +52,7 @@ public final class ViewToolbar extends Composite
{ var group = new Div(components); group.addClassNames(Display.FLEX, FlexDirection.COLUMN, AlignItems.STRETCH, Gap.SMALL, FlexDirection.Breakpoint.Medium.ROW, AlignItems.Breakpoint.Medium.CENTER); + group.addClassName("view-toolbar-group"); return group; } } diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java index db294e4..0a47ee8 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/AdminLayout.java @@ -33,12 +33,16 @@ public final class AdminLayout extends AppLayout { public AdminLayout(SecurityService securityService) { this.securityService = securityService; + addClassName("admin-shell"); setPrimarySection(Section.DRAWER); + getStyle().set("--vaadin-app-layout-drawer-width", "336px"); // Always build the drawer; keep references and toggle visibility on attach and // after navigation headerRef = createHeader(); navRef = new Scroller(createSideNav()); + navRef.addClassName("app-drawer-scroll"); + navRef.setScrollDirection(Scroller.ScrollDirection.VERTICAL); userMenuRef = createUserMenu(); addToDrawer(headerRef, navRef, userMenuRef); @@ -62,18 +66,21 @@ public final class AdminLayout extends AppLayout { private Div createHeader() { var appLogo = VaadinIcon.SHIELD.create(); appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE); + appLogo.addClassName("app-drawer-logo-icon"); var appName = new Span("VotianLT Admin"); appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE); + appName.addClassName("app-drawer-title"); var header = new Div(appLogo, appName); header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER); + header.addClassName("app-drawer-header"); return header; } private Component createSideNav() { var nav = new SideNav(); - nav.addClassNames(Margin.Horizontal.MEDIUM); + nav.addClassName("app-side-nav"); // Only admin-specific menu items SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD)); @@ -98,6 +105,8 @@ public final class AdminLayout extends AppLayout { VerticalLayout navContainer = new VerticalLayout(); navContainer.setPadding(false); navContainer.setSpacing(false); + navContainer.setWidthFull(); + navContainer.addClassName("app-nav-container"); navContainer.add(nav); return navContainer; @@ -106,15 +115,17 @@ public final class AdminLayout extends AppLayout { private Component createUserMenu() { var userMenu = new MenuBar(); userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE); - userMenu.addClassNames(Margin.MEDIUM); + userMenu.addClassName("app-user-menu"); // Dynamically updatable components var avatar = new Avatar(); avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL); avatar.addClassNames(Margin.Right.SMALL); avatar.setColorIndex(1); // Different color for admin + avatar.addClassName("app-user-avatar"); var userNameSpan = new Span(); + userNameSpan.addClassName("app-user-name"); var userMenuItem = userMenu.addItem(avatar); userMenuItem.add(userNameSpan); diff --git a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java index a9b5d75..77db29a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java +++ b/src/main/java/de/assecutor/votianlt/pages/base/ui/view/MainLayout.java @@ -3,10 +3,12 @@ package de.assecutor.votianlt.pages.base.ui.view; import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.DetachEvent; +import com.vaadin.flow.component.HasElement; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.applayout.AppLayout; import com.vaadin.flow.component.avatar.Avatar; import com.vaadin.flow.component.avatar.AvatarVariant; +import com.vaadin.flow.component.grid.GridVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; @@ -24,9 +26,9 @@ import com.vaadin.flow.router.Layout; import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.flow.shared.Registration; import de.assecutor.votianlt.model.User; +import de.assecutor.votianlt.model.Language; import de.assecutor.votianlt.pages.service.AppUserService; import de.assecutor.votianlt.pages.view.EditProfileView; -import de.assecutor.votianlt.model.Language; import de.assecutor.votianlt.security.SecurityService; import de.assecutor.votianlt.service.MessageBadgeUpdateService; import de.assecutor.votianlt.service.MessageService; @@ -51,8 +53,9 @@ public final class MainLayout extends AppLayout { private Scroller navRef; private Component userMenuRef; private TreeGrid tree; - private MenuTreeItem messagesTreeItem; + private long unreadMessagesCount; private Registration badgeUpdateRegistration; + private final Div viewContainer = new Div(); public MainLayout(SecurityService securityService, MessageService messageService, MessageBadgeUpdateService messageBadgeUpdateService, AppUserService appUserService) { @@ -60,10 +63,10 @@ public final class MainLayout extends AppLayout { this.messageService = messageService; this.messageBadgeUpdateService = messageBadgeUpdateService; this.appUserService = appUserService; + addClassName("app-shell"); setPrimarySection(Section.DRAWER); - // Drawer Styles für volle Höhe - getStyle().set("--vaadin-app-layout-drawer-width", "286px"); + getStyle().set("--vaadin-app-layout-drawer-width", "336px"); // Always build the drawer; keep references and toggle visibility on attach and // after navigation @@ -72,10 +75,15 @@ public final class MainLayout extends AppLayout { // Scroller für Navigation mit maximaler Höhe Component sideNav = createSideNav(); navRef = new Scroller(sideNav); + navRef.addClassName("app-drawer-scroll"); + navRef.setScrollDirection(Scroller.ScrollDirection.VERTICAL); userMenuRef = createUserMenu(); addToDrawer(headerRef, navRef, userMenuRef); + viewContainer.addClassName("view-container"); + getElement().appendChild(viewContainer.getElement()); + updateDrawerVisibility(); // Re-check on attach (new UI/session) and on every navigation cycle @@ -96,12 +104,15 @@ public final class MainLayout extends AppLayout { private Div createHeader() { var appLogo = VaadinIcon.CUBES.create(); appLogo.addClassNames(TextColor.PRIMARY, IconSize.LARGE); + appLogo.addClassName("app-drawer-logo-icon"); var appName = new Span("VotianLT"); appName.addClassNames(FontWeight.SEMIBOLD, FontSize.LARGE); + appName.addClassName("app-drawer-title"); var header = new Div(appLogo, appName); header.addClassNames(Display.FLEX, Padding.MEDIUM, Gap.MEDIUM, AlignItems.CENTER); + header.addClassName("app-drawer-header"); return header; } @@ -117,9 +128,6 @@ public final class MainLayout extends AppLayout { MenuTreeItem verwaltungItem = new MenuTreeItem(getTranslation("nav.management"), null, VaadinIcon.COG); MenuTreeItem benutzerItem = new MenuTreeItem(getTranslation("nav.users"), null, VaadinIcon.USER); - // Store reference to messages item for badge updates - messagesTreeItem = nachrichtenItem; - // Add root items treeData.addItem(null, auftragserstellungItem); treeData.addItem(null, nachrichtenItem); @@ -147,50 +155,55 @@ public final class MainLayout extends AppLayout { // Create Tree tree = new TreeGrid<>(); tree.setDataProvider(new TreeDataProvider<>(treeData)); - tree.addClassNames(Margin.Horizontal.MEDIUM); - tree.setHeight("435px"); + tree.addClassName("app-nav-tree"); + tree.addThemeVariants(GridVariant.LUMO_NO_BORDER, GridVariant.LUMO_NO_ROW_BORDERS); + tree.setAllRowsVisible(true); + tree.setWidthFull(); // Custom item renderer to show icon and label with badge - tree.addComponentHierarchyColumn(item -> { + var navColumn = tree.addComponentHierarchyColumn(item -> { HorizontalLayout row = new HorizontalLayout(); row.setAlignItems(FlexComponent.Alignment.CENTER); row.setSpacing(true); row.setPadding(false); row.setMargin(false); - row.setWidthFull(); + row.getStyle().set("max-width", "calc(100% - 4px)"); + row.addClassName("app-nav-row"); // Icon if (item.icon() != null) { Icon icon = item.icon().create(); icon.setSize("var(--lumo-icon-size-s)"); icon.getStyle().set("min-width", "var(--lumo-icon-size-s)"); + icon.addClassName("app-nav-icon"); row.add(icon); } // Label Span label = new Span(item.label()); label.getStyle().set("font-size", "var(--lumo-font-size-s)"); + label.addClassName("app-nav-label"); row.add(label); row.setFlexGrow(1, label); // Badge for messages - if (item == messagesTreeItem && item.badgeCount() > 0) { - Span badge = new Span(String.valueOf(item.badgeCount())); + if ("messages".equals(item.path()) && unreadMessagesCount > 0) { + Span badge = new Span(String.valueOf(unreadMessagesCount)); badge.getElement().getThemeList().add("badge"); - badge.getStyle().set("background-color", "var(--lumo-primary-color)"); - badge.getStyle().set("color", "#ffffff"); - badge.getStyle().set("border-radius", "12px"); - badge.getStyle().set("padding", "2px 8px"); - badge.getStyle().set("font-size", "11px"); - badge.getStyle().set("font-weight", "bold"); - badge.getStyle().set("min-width", "18px"); - badge.getStyle().set("text-align", "center"); - badge.getStyle().set("margin-left", "auto"); + badge.addClassName("app-nav-badge"); row.add(badge); } + // Expand/collapse indicator for parent nodes + Icon chevron = VaadinIcon.ANGLE_RIGHT.create(); + chevron.setSize("var(--lumo-icon-size-s)"); + chevron.addClassName("nav-expand-icon"); + row.add(chevron); + return row; }); + navColumn.setAutoWidth(false); + navColumn.setFlexGrow(1); // Handle selection/navigation tree.addSelectionListener(event -> { @@ -208,6 +221,8 @@ public final class MainLayout extends AppLayout { VerticalLayout navContainer = new VerticalLayout(); navContainer.setPadding(false); navContainer.setSpacing(false); + navContainer.setWidthFull(); + navContainer.addClassName("app-nav-container"); navContainer.add(tree); return navContainer; @@ -221,31 +236,14 @@ public final class MainLayout extends AppLayout { return; } - long unreadCount = resolveUnreadMessageCount(); - - // Get current data provider and update the messages item - TreeDataProvider dataProvider = (TreeDataProvider) tree.getDataProvider(); - TreeData treeData = dataProvider.getTreeData(); - - // Find and update the messages item with new badge count - treeData.getChildren(null).stream().filter(item -> "messages".equals(item.path())).findFirst() - .ifPresent(oldItem -> { - MenuTreeItem newItem = new MenuTreeItem(getTranslation("nav.messages"), "messages", - VaadinIcon.ENVELOPE, unreadCount); - messagesTreeItem = newItem; - // Refresh to show updated badge - dataProvider.refreshAll(); - }); + unreadMessagesCount = resolveUnreadMessageCount(); + tree.getDataProvider().refreshAll(); } /** * Record representing a menu item in the tree */ - private record MenuTreeItem(String label, String path, VaadinIcon icon, long badgeCount) { - MenuTreeItem(String label, String path, VaadinIcon icon) { - this(label, path, icon, 0); - } - + private record MenuTreeItem(String label, String path, VaadinIcon icon) { @Override public boolean equals(Object o) { if (this == o) @@ -294,15 +292,17 @@ public final class MainLayout extends AppLayout { private Component createUserMenu() { var userMenu = new MenuBar(); userMenu.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE); - userMenu.addClassNames(Margin.MEDIUM); + userMenu.addClassName("app-user-menu"); // Dynamisch aktualisierbare Komponenten var avatar = new Avatar(); avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL); avatar.addClassNames(Margin.Right.SMALL); avatar.setColorIndex(5); + avatar.addClassName("app-user-avatar"); var userNameSpan = new Span(); + userNameSpan.addClassName("app-user-name"); var userMenuItem = userMenu.addItem(avatar); userMenuItem.add(userNameSpan); @@ -335,12 +335,19 @@ public final class MainLayout extends AppLayout { // Drawer-Layout anpassen nach kurzer Verzögerung ui.access(() -> { getElement().executeJs( - "setTimeout(() => {" + " const drawer = this.shadowRoot?.querySelector('[part=drawer]');" - + " if (drawer) {" + " drawer.style.display = 'flex';" - + " drawer.style.flexDirection = 'column';" + " drawer.style.height = '100vh';" - + " const scroller = drawer.querySelector('vaadin-scroller');" + " if (scroller) {" - + " scroller.style.flex = '1 1 auto';" + " scroller.style.minHeight = '0';" - + " }" + " }" + "}, 100);"); + "setTimeout(() => {" + + " const drawer = this.shadowRoot?.querySelector('[part=drawer]');" + + " if (drawer) {" + + " drawer.style.display = 'flex';" + + " drawer.style.flexDirection = 'column';" + + " drawer.style.height = '100vh';" + + " const scroller = drawer.querySelector('vaadin-scroller');" + + " if (scroller) {" + + " scroller.style.flex = '1 1 auto';" + + " scroller.style.minHeight = '0';" + + " }" + + " }" + + "}, 100);"); }); // Apply user's preferred language immediately after login @@ -393,6 +400,14 @@ public final class MainLayout extends AppLayout { }; } + @Override + public void showRouterLayoutContent(HasElement content) { + viewContainer.getElement().removeAllChildren(); + if (content != null) { + viewContainer.getElement().appendChild(content.getElement()); + } + } + @Override protected void onDetach(DetachEvent detachEvent) { if (badgeUpdateRegistration != null) { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java index 33c9012..df642f1 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddAppUserView.java @@ -42,6 +42,7 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle { setSizeFull(); setPadding(true); setSpacing(true); + addClassName("form-page"); // Set field labels via i18n designationField.setLabel(getTranslation("addappuser.designation")); @@ -62,17 +63,17 @@ public class AddAppUserView extends VerticalLayout implements HasDynamicTitle { contentContainer.setMaxWidth("90%"); contentContainer.setSpacing(true); contentContainer.setPadding(true); - contentContainer.getStyle().set("background", "var(--lumo-contrast-5pct)"); - contentContainer.getStyle().set("border-radius", "var(--lumo-border-radius)"); - contentContainer.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)"); + contentContainer.addClassNames("form-shell", "form-card"); // Header with title and back button HorizontalLayout header = new HorizontalLayout(); header.setAlignItems(FlexComponent.Alignment.CENTER); header.setSpacing(true); + header.addClassName("form-header"); H2 title = new H2(getTranslation("addappuser.title")); title.getStyle().set("margin", "0"); + title.addClassName("form-title"); Button backButton = new Button(getTranslation("button.back"), new Icon(VaadinIcon.ARROW_LEFT)); backButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java index 1f0b8a7..ac51d70 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddCompanyView.java @@ -73,11 +73,14 @@ public class AddCompanyView extends Main implements HasDynamicTitle { // Zentriere die Inhalte vertikal und horizontal formLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); formLayout.setSpacing(true); - formLayout.setSizeUndefined(); // Inhalt eng setzen + formLayout.setWidthFull(); + formLayout.setMaxWidth("620px"); + formLayout.addClassNames("form-shell", "form-card"); setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassName("form-page"); add(new ViewToolbar(getTranslation("addcompany.title"))); add(formLayout); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java index d5ad1e5..35febf6 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddCustomerView.java @@ -150,11 +150,13 @@ public class AddCustomerView extends Main implements HasDynamicTitle { VerticalLayout container = new VerticalLayout(); container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); container.setSpacing(true); + container.addClassNames("form-shell", "form-card"); container.add(formLayout); setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassName("form-page"); add(new ViewToolbar(getTranslation("addcustomer.title"))); add(container); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java index 95b2ff4..73be8ab 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AddJobView.java @@ -10,7 +10,6 @@ import com.vaadin.flow.component.UI; import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.timepicker.TimePicker; import com.vaadin.flow.component.html.H3; import com.vaadin.flow.component.html.Main; @@ -67,6 +66,7 @@ import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationTile; import de.assecutor.votianlt.pages.base.ui.component.StationTile; import de.assecutor.votianlt.pages.base.ui.component.PickupStationDialog; import de.assecutor.votianlt.pages.base.ui.component.DeliveryStationDialog; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import java.time.LocalDate; import java.time.LocalTime; import java.time.format.DateTimeFormatter; @@ -338,9 +338,9 @@ public class AddJobView extends Main implements HasDynamicTitle { setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassNames("form-page", "add-job-view"); - H2 title = new H2(getTranslation("addjob.title")); - add(title); + add(new ViewToolbar(getTranslation("addjob.title"))); // Add content directly (no tabs) add(createCustomerAndAddressesTab()); @@ -350,6 +350,7 @@ public class AddJobView extends Main implements HasDynamicTitle { submitButtonLayout.setWidthFull(); submitButtonLayout.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); submitButtonLayout.setPadding(true); + submitButtonLayout.addClassNames("surface-panel", "form-shell"); submitButtonLayout.add(submitButton); submitButtonLayout.setVisible(false); @@ -361,14 +362,13 @@ public class AddJobView extends Main implements HasDynamicTitle { tabContent.setWidthFull(); tabContent.setPadding(true); tabContent.setSpacing(true); + tabContent.addClassNames("form-shell", "form-card"); // 3x3 Grid container for station tiles stationsGridContainer = new Div(); - stationsGridContainer.getStyle().set("display", "grid"); - stationsGridContainer.getStyle().set("grid-template-columns", "repeat(4, 1fr)"); - stationsGridContainer.getStyle().set("gap", "var(--lumo-space-m)"); stationsGridContainer.getStyle().set("padding", "var(--lumo-space-s)"); stationsGridContainer.setWidthFull(); + stationsGridContainer.addClassName("stations-grid"); // Pickup tile (always present) pickupTile = new StationTile(StationTile.StationType.PICKUP, 0, getTranslation("addjob.section.pickup"), false); @@ -394,6 +394,7 @@ public class AddJobView extends Main implements HasDynamicTitle { priceAndDetailsSection.setPadding(false); priceAndDetailsSection.setSpacing(true); priceAndDetailsSection.setVisible(false); + priceAndDetailsSection.addClassName("content-panel"); // "Stationen übernehmen" Button applyStationsButton = new Button(getTranslation("addjob.stations.apply")); @@ -413,6 +414,7 @@ public class AddJobView extends Main implements HasDynamicTitle { routeInfoBox.setWidthFull(); routeInfoBox.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); routeInfoBox.setVisible(false); // Initial versteckt + routeInfoBox.addClassName("route-card"); H3 routeTitle = new H3(getTranslation("addjob.route.title")); routeTitle.getStyle().set("margin", "0"); @@ -456,6 +458,7 @@ public class AddJobView extends Main implements HasDynamicTitle { manualRouteInputBox.setWidthFull(); manualRouteInputBox.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); manualRouteInputBox.setVisible(true); // Initial sichtbar + manualRouteInputBox.addClassName("route-card"); H3 manualRouteTitle = new H3(getTranslation("addjob.route.manual.title")); manualRouteTitle.getStyle().set("margin", "0"); @@ -514,6 +517,7 @@ public class AddJobView extends Main implements HasDynamicTitle { servicesGrid.setWidthFull(); servicesGrid.setHeight("250px"); servicesGrid.setItems(selectedServices); + servicesGrid.addClassName("data-grid"); servicesGrid.addColumn(entry -> entry.getService().getName()).setHeader(getTranslation("common.service")) .setSortable(true); @@ -585,6 +589,7 @@ public class AddJobView extends Main implements HasDynamicTitle { summaryLayout.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); summaryLayout.setWidthFull(); summaryLayout.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); + summaryLayout.addClassName("summary-card"); H3 summaryTitle = new H3(getTranslation("addjob.summary.title")); summaryTitle.getStyle().set("margin", "0"); @@ -635,6 +640,7 @@ public class AddJobView extends Main implements HasDynamicTitle { private Div createAddStationButton() { Div button = new Div(); + button.addClassName("add-station-tile"); button.getStyle().set("border", "2px dashed var(--lumo-contrast-30pct)"); button.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); button.getStyle().set("display", "flex"); @@ -664,12 +670,14 @@ public class AddJobView extends Main implements HasDynamicTitle { private Div createStationSlot(Component content, Span distanceChip) { Div slot = new Div(); + slot.addClassName("station-slot"); slot.getStyle().set("display", "flex"); slot.getStyle().set("flex-direction", "column"); slot.getStyle().set("align-items", "center"); slot.getStyle().set("gap", "var(--lumo-space-s)"); slot.setWidthFull(); + content.getElement().getClassList().add("station-slot-content"); content.getElement().getStyle().set("width", "100%"); slot.add(content); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java b/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java index c8ee561..e892917 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AdminDashboardView.java @@ -61,24 +61,29 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM); + addClassName("dashboard-view"); // Header H1 title = new H1(getTranslation("admindashboard.title")); title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, LumoUtility.Margin.Top.NONE); + title.addClassName("dashboard-title"); HorizontalLayout header = new HorizontalLayout(title); header.setAlignItems(FlexComponent.Alignment.CENTER); header.setWidthFull(); + header.addClassNames("surface-panel", "dashboard-header"); // Statistics container statisticsContainer = new Div(); statisticsContainer.setSizeFull(); + statisticsContainer.addClassName("dashboard-content"); // Content container VerticalLayout content = new VerticalLayout(header, statisticsContainer); content.setSizeFull(); content.setPadding(false); content.setSpacing(true); + content.addClassName("dashboard-sections"); add(content); @@ -114,6 +119,7 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { VerticalLayout mainLayout = new VerticalLayout(); mainLayout.setPadding(false); mainLayout.setSpacing(true); + mainLayout.addClassName("dashboard-sections"); // System overview section mainLayout.add(createSystemOverviewSection()); @@ -136,15 +142,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { private VerticalLayout createSystemOverviewSection() { VerticalLayout section = new VerticalLayout(); section.setPadding(false); - section.addClassName(LumoUtility.Background.CONTRAST_5); - section.getStyle().set("border-radius", "8px").set("padding", "1rem"); + section.addClassNames("surface-panel", "dashboard-section"); H3 title = new H3(getTranslation("admindashboard.section.overview")); - title.addClassName(LumoUtility.Margin.Bottom.MEDIUM); + title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title"); - HorizontalLayout cards = new HorizontalLayout(); - cards.setWidthFull(); - cards.setSpacing(true); + Div cards = new Div(); + cards.addClassName("dashboard-grid"); // Total jobs card long totalJobs = jobRepository.count(); @@ -173,15 +177,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { private VerticalLayout createJobStatisticsSection() { VerticalLayout section = new VerticalLayout(); section.setPadding(false); - section.addClassName(LumoUtility.Background.CONTRAST_5); - section.getStyle().set("border-radius", "8px").set("padding", "1rem"); + section.addClassNames("surface-panel", "dashboard-section"); H3 title = new H3(getTranslation("admindashboard.section.jobs")); - title.addClassName(LumoUtility.Margin.Bottom.MEDIUM); + title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title"); - HorizontalLayout cards = new HorizontalLayout(); - cards.setWidthFull(); - cards.setSpacing(true); + Div cards = new Div(); + cards.addClassName("dashboard-grid"); // Jobs by status try { @@ -214,15 +216,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { private VerticalLayout createTaskStatisticsSection() { VerticalLayout section = new VerticalLayout(); section.setPadding(false); - section.addClassName(LumoUtility.Background.CONTRAST_5); - section.getStyle().set("border-radius", "8px").set("padding", "1rem"); + section.addClassNames("surface-panel", "dashboard-section"); H3 title = new H3(getTranslation("admindashboard.section.tasks")); - title.addClassName(LumoUtility.Margin.Bottom.MEDIUM); + title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title"); - HorizontalLayout cards = new HorizontalLayout(); - cards.setWidthFull(); - cards.setSpacing(true); + Div cards = new Div(); + cards.addClassName("dashboard-grid"); // Total tasks long totalTasks = taskRepository.count(); @@ -251,15 +251,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { private VerticalLayout createUserStatisticsSection() { VerticalLayout section = new VerticalLayout(); section.setPadding(false); - section.addClassName(LumoUtility.Background.CONTRAST_5); - section.getStyle().set("border-radius", "8px").set("padding", "1rem"); + section.addClassNames("surface-panel", "dashboard-section"); H3 title = new H3(getTranslation("admindashboard.section.users")); - title.addClassName(LumoUtility.Margin.Bottom.MEDIUM); + title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title"); - HorizontalLayout cards = new HorizontalLayout(); - cards.setWidthFull(); - cards.setSpacing(true); + Div cards = new Div(); + cards.addClassName("dashboard-grid"); // Content statistics long totalPhotos = photoRepository.count(); @@ -285,15 +283,13 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { private VerticalLayout createSystemHealthSection() { VerticalLayout section = new VerticalLayout(); section.setPadding(false); - section.addClassName(LumoUtility.Background.CONTRAST_5); - section.getStyle().set("border-radius", "8px").set("padding", "1rem"); + section.addClassNames("surface-panel", "dashboard-section"); H3 title = new H3(getTranslation("admindashboard.section.health")); - title.addClassName(LumoUtility.Margin.Bottom.MEDIUM); + title.addClassNames(LumoUtility.Margin.Bottom.MEDIUM, "dashboard-section-title"); - HorizontalLayout cards = new HorizontalLayout(); - cards.setWidthFull(); - cards.setSpacing(true); + Div cards = new Div(); + cards.addClassName("dashboard-grid"); // Database connection status try { @@ -327,31 +323,31 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { private Div createStatCard(String title, String value, VaadinIcon icon, String color) { Div card = new Div(); - card.addClassName(LumoUtility.Background.BASE); - card.getStyle().set("border-radius", "8px").set("padding", "1rem") - .set("box-shadow", "0 2px 4px rgba(0,0,0,0.1)").set("min-width", "200px") - .set("border-left", "4px solid var(--lumo-" + color + "-color, #007bff)"); + card.addClassNames("dashboard-stat-card", "accent-" + color); HorizontalLayout header = new HorizontalLayout(); header.setAlignItems(FlexComponent.Alignment.CENTER); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); + header.addClassName("dashboard-stat-card-header"); Icon cardIcon = icon.create(); - cardIcon.getStyle().set("color", "var(--lumo-" + color + "-color, #007bff)"); + cardIcon.addClassNames("dashboard-stat-icon", "accent-" + color); Span titleSpan = new Span(title); titleSpan.addClassName(LumoUtility.FontSize.SMALL); - titleSpan.getStyle().set("color", "var(--lumo-secondary-text-color)"); + titleSpan.addClassName("dashboard-stat-title"); header.add(titleSpan, cardIcon); Span valueSpan = new Span(value); valueSpan.addClassName(LumoUtility.FontSize.XLARGE); valueSpan.addClassName(LumoUtility.FontWeight.BOLD); + valueSpan.addClassName("dashboard-stat-value"); VerticalLayout content = new VerticalLayout(header, valueSpan); content.setPadding(false); content.setSpacing(false); + content.addClassName("dashboard-stat-content"); card.add(content); return card; @@ -361,4 +357,4 @@ public class AdminDashboardView extends Main implements HasDynamicTitle { public String getPageTitle() { return getTranslation("page.title.admin.dashboard"); } -} \ No newline at end of file +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java index 71e9f9e..7995c6a 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AdminPricetableView.java @@ -1,13 +1,14 @@ package de.assecutor.votianlt.pages.view; import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.Route; import de.assecutor.votianlt.model.PriceTable; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.pages.base.ui.view.AdminLayout; import de.assecutor.votianlt.repository.PriceTableRepository; import jakarta.annotation.security.RolesAllowed; @@ -28,13 +29,13 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit setPadding(false); getStyle().set("margin", "14px"); setWidth("90%"); - - H2 title = new H2(getTranslation("adminpricetable.title")); - add(title); + addClassName("admin-form-view"); + add(new ViewToolbar(getTranslation("adminpricetable.title"))); VerticalLayout fieldsLayout = new VerticalLayout(); fieldsLayout.setSpacing(true); fieldsLayout.setPadding(false); + fieldsLayout.addClassNames("form-shell", "form-card"); monthlyBasePackage = new TextField(); monthlyBasePackage.setLabel(getTranslation("adminpricetable.field.monthly")); @@ -58,8 +59,12 @@ public class AdminPricetableView extends VerticalLayout implements HasDynamicTit Button saveButton = new Button(getTranslation("button.savechanges")); saveButton.getStyle().set("margin-top", "20px"); saveButton.addClickListener(e -> savePriceTable()); + saveButton.addThemeVariants(com.vaadin.flow.component.button.ButtonVariant.LUMO_PRIMARY); - add(saveButton); + Div actions = new Div(saveButton); + actions.addClassNames("form-shell", "simple-card"); + + add(fieldsLayout, actions); // Load existing data loadPriceTable(); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java index f595d85..51efaa6 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AppUserView.java @@ -3,15 +3,14 @@ package de.assecutor.votianlt.pages.view; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; -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.HasDynamicTitle; import com.vaadin.flow.router.Route; import de.assecutor.votianlt.model.AppUser; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.pages.service.AppUserService; import jakarta.annotation.security.RolesAllowed; import org.springframework.beans.factory.annotation.Autowired; @@ -30,26 +29,18 @@ public class AppUserView extends VerticalLayout implements HasDynamicTitle { setSizeFull(); setPadding(true); setSpacing(true); - - // Header mit Titel und Button - HorizontalLayout header = new HorizontalLayout(); - header.setWidthFull(); - header.setAlignItems(FlexComponent.Alignment.CENTER); - - H2 title = new H2(getTranslation("appuser.title")); - title.getStyle().set("margin", "0"); + addClassName("data-view"); Button addButton = new Button(getTranslation("appuser.button.add"), new Icon(VaadinIcon.PLUS)); addButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); addButton.addClickListener(e -> navigateToAddAppUser()); - header.add(title, addButton); - header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); - add(header); + add(new ViewToolbar(getTranslation("appuser.title"), addButton)); // Grid für App-Nutzer appUserGrid = new Grid<>(AppUser.class, false); appUserGrid.setSizeFull(); + appUserGrid.addClassName("data-grid"); // Grid-Spalten konfigurieren appUserGrid.addColumn(AppUser::getBezeichnung).setHeader(getTranslation("appuser.column.designation")) @@ -75,7 +66,11 @@ public class AppUserView extends VerticalLayout implements HasDynamicTitle { } }); - add(appUserGrid); + Div gridPanel = new Div(appUserGrid); + gridPanel.addClassNames("surface-panel", "data-grid-panel"); + gridPanel.setSizeFull(); + + add(gridPanel); // Daten laden loadAppUsers(); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java index 64b24a0..c1a7635 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/AuthenticatedStartView.java @@ -31,8 +31,9 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic setSizeFull(); setPadding(false); - setSpacing(false); + setSpacing(true); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); + addClassName("dashboard-home-view"); // Hero Section for authenticated users add(createAuthenticatedHeroSection()); @@ -53,10 +54,8 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic heroSection.setPadding(true); heroSection.setSpacing(true); heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - heroSection.getStyle().set("background", - "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))"); - heroSection.getStyle().set("min-height", "300px"); heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + heroSection.addClassNames("page-shell", "hero-panel", "landing-hero"); // Welcome message for authenticated user User currentUser = securityService.getCurrentDatabaseUser(); @@ -64,14 +63,10 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic ? currentUser.getFirstname() + " " + currentUser.getName() : securityService.getCurrentUsername(); H1 welcomeTitle = new H1(getTranslation("dashboard.welcome", displayName)); - welcomeTitle.getStyle().set("text-align", "center"); - welcomeTitle.getStyle().set("color", "var(--lumo-primary-text-color)"); - welcomeTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)"); + welcomeTitle.addClassName("hero-panel-title"); Paragraph welcomeDescription = new Paragraph(getTranslation("dashboard.description")); - welcomeDescription.getStyle().set("text-align", "center"); - welcomeDescription.getStyle().set("max-width", "600px"); - welcomeDescription.getStyle().set("font-size", "var(--lumo-font-size-l)"); + welcomeDescription.addClassName("hero-panel-text"); heroSection.add(welcomeTitle, welcomeDescription); return heroSection; @@ -83,26 +78,18 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic systemSection.setPadding(true); systemSection.setSpacing(true); systemSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - systemSection.getStyle().set("background-color", "var(--lumo-base-color)"); + systemSection.addClassNames("page-shell", "surface-panel", "section-panel"); // Section Header H2 systemTitle = new H2(getTranslation("dashboard.system.title")); - systemTitle.getStyle().set("color", "var(--lumo-primary-color)"); - systemTitle.getStyle().set("text-align", "center"); + systemTitle.addClassName("section-title"); Paragraph systemIntro = new Paragraph(getTranslation("dashboard.system.intro")); - systemIntro.getStyle().set("text-align", "center"); - systemIntro.getStyle().set("max-width", "800px"); - systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)"); + systemIntro.addClassName("section-intro"); // Features Grid - using Div with CSS flexbox for proper centering Div featuresGrid = new Div(); - featuresGrid.getStyle().set("display", "flex"); - featuresGrid.getStyle().set("flex-wrap", "wrap"); - featuresGrid.getStyle().set("justify-content", "center"); - featuresGrid.getStyle().set("align-items", "stretch"); - featuresGrid.getStyle().set("gap", "var(--lumo-space-l)"); - featuresGrid.getStyle().set("width", "100%"); + featuresGrid.addClassName("tile-grid"); // Feature Cards featuresGrid.add( @@ -122,32 +109,18 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic card.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); card.setPadding(true); card.setSpacing(true); - card.getStyle().set("background-color", "var(--lumo-base-color)"); - card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - card.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - card.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)"); - card.setWidth("300px"); - card.setMinWidth("300px"); - card.setMaxWidth("300px"); - card.setHeight("100%"); - card.getStyle().set("flex-grow", "0"); - card.getStyle().set("flex-shrink", "0"); + card.setWidthFull(); + card.addClassName("feature-card"); Icon icon = iconType.create(); icon.setSize("48px"); - icon.getStyle().set("color", "var(--lumo-primary-color)"); - icon.getStyle().set("margin-bottom", "var(--lumo-space-m)"); + icon.addClassName("feature-card-icon"); H3 cardTitle = new H3(title); - cardTitle.getStyle().set("text-align", "center"); - cardTitle.getStyle().set("margin", "0"); - cardTitle.getStyle().set("color", "var(--lumo-primary-text-color)"); + cardTitle.addClassName("feature-card-title"); Paragraph cardDescription = new Paragraph(description); - cardDescription.getStyle().set("text-align", "center"); - cardDescription.getStyle().set("font-size", "var(--lumo-font-size-s)"); - cardDescription.getStyle().set("color", "var(--lumo-secondary-text-color)"); - cardDescription.getStyle().set("margin", "0"); + cardDescription.addClassName("feature-card-description"); card.add(icon, cardTitle, cardDescription); return card; @@ -159,15 +132,13 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic appSection.setPadding(true); appSection.setSpacing(true); appSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - appSection.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); + appSection.addClassNames("page-shell", "surface-panel", "app-overview-panel"); H2 appTitle = new H2(getTranslation("dashboard.app.title")); - appTitle.getStyle().set("color", "var(--lumo-primary-color)"); - appTitle.getStyle().set("text-align", "center"); + appTitle.addClassName("section-title"); Paragraph appDescription = new Paragraph(getTranslation("dashboard.app.description")); - appDescription.getStyle().set("text-align", "center"); - appDescription.getStyle().set("max-width", "600px"); + appDescription.addClassName("section-intro"); appSection.add(appTitle, appDescription); return appSection; @@ -179,18 +150,14 @@ public class AuthenticatedStartView extends VerticalLayout implements HasDynamic footer.setPadding(true); footer.setSpacing(true); footer.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - footer.getStyle().set("background-color", "var(--lumo-contrast-10pct)"); - footer.getStyle().set("border-top", "1px solid var(--lumo-contrast-20pct)"); - footer.getStyle().set("margin-top", "auto"); + footer.addClassNames("page-shell", "surface-panel", "footer-panel"); HorizontalLayout footerContent = new HorizontalLayout(); footerContent.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); footerContent.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); Paragraph copyright = new Paragraph(getTranslation("dashboard.footer.copyright")); - copyright.getStyle().set("color", "var(--lumo-secondary-text-color)"); - copyright.getStyle().set("font-size", "var(--lumo-font-size-s)"); - copyright.getStyle().set("margin", "0"); + copyright.addClassName("footer-version"); footerContent.add(copyright); footer.add(footerContent); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java index 1a473e0..5c564c2 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CreateInvoiceView.java @@ -128,6 +128,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter setSizeFull(); setPadding(true); setSpacing(true); + addClassName("data-view"); } /** @@ -216,9 +217,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private Div createJobDetailsSection() { Div section = new Div(); - section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box"); + section.setWidthFull(); + section.addClassName("invoice-summary-card"); + section.getStyle().set("margin-bottom", "var(--lumo-space-m)"); H3 sectionTitle = new H3(getTranslation("createinvoice.section.job")); section.add(sectionTitle); @@ -243,13 +244,11 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private Div createRouteInfoSection() { Div section = new Div(); - section.getStyle().set("border", "1px solid var(--lumo-primary-color-50pct)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box") - .set("background-color", "var(--lumo-primary-color-10pct)"); + section.setWidthFull(); + section.addClassNames("detail-card", "detail-card--accent"); + section.getStyle().set("margin-bottom", "var(--lumo-space-m)"); H3 sectionTitle = new H3(getTranslation("createinvoice.section.route")); - sectionTitle.getStyle().set("color", "var(--lumo-primary-text-color)"); section.add(sectionTitle); VerticalLayout routeInfo = new VerticalLayout(); @@ -275,9 +274,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private Div createServicesSelectionSection() { servicesSection = new Div(); - servicesSection.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box"); + servicesSection.setWidthFull(); + servicesSection.addClassName("services-panel"); + servicesSection.getStyle().set("margin-bottom", "var(--lumo-space-m)"); H3 sectionTitle = new H3(getTranslation("createinvoice.section.services")); servicesSection.add(sectionTitle); @@ -286,6 +285,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter servicesGrid = new Grid<>(); servicesGrid.setWidthFull(); servicesGrid.setAllRowsVisible(true); + servicesGrid.addClassName("data-grid"); // Service name column (read-only) servicesGrid.addColumn(row -> { @@ -333,9 +333,9 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private Div createSummarySection() { Div section = new Div(); - section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box"); + section.setWidthFull(); + section.addClassName("invoice-section-card"); + section.getStyle().set("margin-bottom", "var(--lumo-space-m)"); H3 sectionTitle = new H3(getTranslation("createinvoice.section.summary")); section.add(sectionTitle); @@ -347,7 +347,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter BigDecimal totalAmount = netAmount.add(vatAmount); Div priceTable = new Div(); - priceTable.getStyle().set("width", "100%"); + priceTable.addClassName("price-table"); priceTable.add(createPriceRow(getTranslation("createinvoice.summary.net") + ":", netAmount.setScale(2, RoundingMode.HALF_UP) + " €", false)); @@ -360,17 +360,16 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter private Div createPriceRow(String label, String value, boolean bold) { Div row = new Div(); - row.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + row.addClassName("price-row"); + if (bold) { + row.addClassName("price-row--strong"); + } Span labelSpan = new Span(label); - labelSpan.getStyle().set("padding-right", "8px"); + labelSpan.addClassName("price-row-label"); Span valueSpan = new Span(value); - valueSpan.getStyle().set("white-space", "nowrap"); - if (bold) { - labelSpan.getStyle().set("font-weight", "bold"); - valueSpan.getStyle().set("font-weight", "bold"); - } + valueSpan.addClassName("price-row-value"); row.add(labelSpan, valueSpan); return row; @@ -668,10 +667,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter IFrame pdfFrame = new IFrame(); pdfFrame.setWidth("100%"); pdfFrame.setHeight("100%"); + pdfFrame.addClassName("invoice-generator-frame"); String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes); pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf); - pdfFrame.getStyle().set("border", "none"); Button closeButton = new Button(getTranslation("button.close"), e -> pdfDialog.close()); closeButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); @@ -705,10 +704,10 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter IFrame pdfFrame = new IFrame(); pdfFrame.setWidth("100%"); pdfFrame.setHeight("100%"); + pdfFrame.addClassName("invoice-generator-frame"); String base64Pdf = java.util.Base64.getEncoder().encodeToString(pdfBytes); pdfFrame.getElement().setAttribute("src", "data:application/pdf;base64," + base64Pdf); - pdfFrame.getStyle().set("border", "none"); Button downloadButton = new Button("Herunterladen", e -> { parent.getElement() diff --git a/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java b/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java index ddcac14..80e0022 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/CustomersView.java @@ -4,7 +4,7 @@ import com.vaadin.flow.component.UI; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Main; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.router.HasDynamicTitle; @@ -12,6 +12,7 @@ import com.vaadin.flow.router.Menu; import com.vaadin.flow.router.Route; import com.vaadin.flow.theme.lumo.LumoUtility; import de.assecutor.votianlt.model.Customer; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.pages.service.CustomerService; import java.time.Clock; @@ -38,15 +39,21 @@ public class CustomersView extends Main implements HasDynamicTitle { todoGrid.setItems(query -> todoService.list(toSpringPageRequest(query)).stream()); todoGrid.addColumn(Customer::getCompanyName).setHeader(getTranslation("customers.column.company")); todoGrid.setSizeFull(); + todoGrid.addClassName("data-grid"); setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassName("data-view"); - H2 title = new H2(getTranslation("customers.title")); - add(title); + ViewToolbar toolbar = new ViewToolbar(getTranslation("customers.title"), createBtn); + createBtn.addClassName("toolbar-primary-action"); - add(todoGrid); + Div gridPanel = new Div(todoGrid); + gridPanel.addClassNames("surface-panel", "data-grid-panel"); + gridPanel.setSizeFull(); + + add(toolbar, gridPanel); } private void addCustomer() { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java index 912251c..dc0d13e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/EditAppUserView.java @@ -46,6 +46,7 @@ public class EditAppUserView extends VerticalLayout implements HasUrlParameter(CustomerInvoice.class, false); invoiceGrid.setWidthFull(); + invoiceGrid.addClassName("data-grid"); invoiceGrid.addColumn(invoice -> firstNonBlank(invoice.getInvoiceNumber(), invoice.getId())) .setHeader(getTranslation("invoices.column.number")).setAutoWidth(true); invoiceGrid.addColumn(this::getRecipientLabel).setHeader(getTranslation("invoices.column.customer")) @@ -66,7 +68,11 @@ public class InvoicesView extends VerticalLayout implements HasDynamicTitle { }); loadInvoices(); - add(invoiceGrid); + Div gridPanel = new Div(invoiceGrid); + gridPanel.addClassNames("surface-panel", "data-grid-panel"); + gridPanel.setWidthFull(); + + add(gridPanel); } private void loadInvoices() { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java index f532e02..bd7b2fd 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobHistoryView.java @@ -55,6 +55,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassName("data-view"); add(new ViewToolbar(getTranslation("jobhistory.title"))); @@ -62,6 +63,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has content.setSpacing(true); content.setPadding(true); content.setWidthFull(); + content.addClassNames("form-shell", "form-card"); add(content); } @@ -131,9 +133,8 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has private Div createJobInfoBox(Job job) { Div infoBox = new Div(); - infoBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("background-color", "var(--lumo-base-color)").set("margin-bottom", "var(--lumo-space-m)"); + infoBox.addClassName("summary-card"); + infoBox.getStyle().set("margin-bottom", "var(--lumo-space-m)"); VerticalLayout infoContent = new VerticalLayout(); infoContent.setPadding(false); @@ -169,11 +170,9 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has private Div createHistoryEntryCard(JobHistory entry) { Div card = new Div(); - card.getStyle().set("border", "1px solid var(--lumo-contrast-10pct)") - .set("border-left", "4px solid " + getTypeColor(entry.getChangeType())) - .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-m)") - .set("margin-bottom", "var(--lumo-space-s)").set("background-color", "var(--lumo-base-color)") - .set("width", "100%").set("box-sizing", "border-box"); + card.addClassName("timeline-entry-card"); + card.setWidthFull(); + card.getStyle().set("--timeline-accent", getTypeColor(entry.getChangeType())); // Header row with icon, reason and timestamp HorizontalLayout headerRow = new HorizontalLayout(); @@ -185,11 +184,10 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has Span reason = new Span( entry.getReason() != null ? entry.getReason() : getTranslation("jobhistory.entry.unknown")); - reason.getStyle().set("font-weight", "500"); + reason.addClassName("timeline-reason"); Span timestamp = new Span(formatDateTime(entry.getTimestamp())); - timestamp.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-size", - "var(--lumo-font-size-s)"); + timestamp.addClassName("timeline-timestamp"); HorizontalLayout leftSide = new HorizontalLayout(typeIcon, reason); leftSide.setAlignItems(HorizontalLayout.Alignment.CENTER); @@ -206,8 +204,7 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has // Description if (entry.getDescription() != null && !entry.getDescription().isBlank()) { Span description = new Span(entry.getDescription()); - description.getStyle().set("color", "var(--lumo-body-text-color)").set("margin-top", "var(--lumo-space-xs)") - .set("display", "block"); + description.addClassName("timeline-description"); cardContent.add(description); } @@ -465,11 +462,8 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has private Div createBarcodeBox(String barcodeValue) { Div barcodeBox = new Div(); - barcodeBox.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-s)").set("padding", "var(--lumo-space-xs)") - .set("background-color", "var(--lumo-contrast-5pct)").set("font-family", "monospace") - .set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)") - .set("word-break", "break-all"); + barcodeBox.addClassNames("detail-card", "detail-card--code"); + barcodeBox.getStyle().set("font-size", "var(--lumo-font-size-s)").set("margin-bottom", "var(--lumo-space-xs)"); barcodeBox.add(new Span(barcodeValue)); return barcodeBox; @@ -580,4 +574,4 @@ public class JobHistoryView extends Main implements HasUrlParameter, Has log.error("Error showing enlarged signature: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java index eaf608a..a66fdf0 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/JobSummaryView.java @@ -120,11 +120,13 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassName("data-view"); content = new VerticalLayout(); content.setSpacing(true); content.setPadding(true); content.setWidthFull(); + content.addClassNames("form-shell", "form-card"); } @Override @@ -282,7 +284,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has PriceCalculationResult priceResult = calculatePriceFromServices(job); Div priceTable = new Div(); - priceTable.getStyle().set("width", "100%"); + priceTable.addClassName("price-table"); priceTable.add(createPriceRow(getTranslation("jobsummary.info.netto") + ":", formatPrice(priceResult.netAmount()), false)); @@ -359,9 +361,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private VerticalLayout borderedBox() { VerticalLayout box = new VerticalLayout(); - box.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - box.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - box.getStyle().set("background-color", "var(--lumo-base-color)"); + box.addClassName("summary-card"); box.setPadding(true); box.setSpacing(false); return box; @@ -369,10 +369,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private Div createStationTilesSection(Job job, List cargoItems, List tasks) { Div stationGrid = new Div(); - stationGrid.getStyle().set("display", "grid"); - stationGrid.getStyle().set("grid-template-columns", "repeat(auto-fit, minmax(220px, 1fr))"); - stationGrid.getStyle().set("gap", "var(--lumo-space-m)"); stationGrid.setWidthFull(); + stationGrid.addClassName("stations-grid"); stationGrid.add(createPickupSummaryTile(job, cargoItems, tasks)); @@ -683,6 +681,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has dialogContent.setSpacing(true); dialogContent.setWidthFull(); dialogContent.getStyle().set("min-width", "0"); + dialogContent.addClassName("dialog-content-panel"); H4 header = new H4(getTranslation("jobsummary.section.tasks")); header.getStyle().set("margin", "0"); @@ -775,17 +774,16 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private Div createPriceRow(String label, String value, boolean bold) { Div row = new Div(); - row.getStyle().set("display", "flex").set("justify-content", "space-between").set("padding", "4px 0"); + row.addClassName("price-row"); + if (bold) { + row.addClassName("price-row--strong"); + } Span labelSpan = new Span(label); - labelSpan.getStyle().set("padding-right", "8px"); + labelSpan.addClassName("price-row-label"); Span valueSpan = new Span(value); - valueSpan.getStyle().set("white-space", "nowrap"); - if (bold) { - labelSpan.getStyle().set("font-weight", "bold"); - valueSpan.getStyle().set("font-weight", "bold"); - } + valueSpan.addClassName("price-row-value"); row.add(labelSpan, valueSpan); return row; @@ -860,15 +858,11 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has Div map = new Div(); map.setWidthFull(); map.setHeight("520px"); - map.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - map.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); + map.addClassName("job-summary-map"); Div routeInfo = new Div(); routeInfo.setWidthFull(); - routeInfo.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - routeInfo.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - routeInfo.getStyle().set("padding", "var(--lumo-space-m)"); - routeInfo.getStyle().set("background-color", "var(--lumo-base-color)"); + routeInfo.addClassName("route-card"); content.add(map, routeInfo); @@ -1110,6 +1104,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has VerticalLayout dialogContent = new VerticalLayout(); dialogContent.setPadding(true); dialogContent.setSpacing(true); + dialogContent.addClassName("dialog-content-panel"); // Header H4 header = new H4("Aufgaben-Details"); @@ -1238,12 +1233,9 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has if (svgContent != null && !svgContent.isBlank()) { // Create a div to hold the SVG Div svgContainer = new Div(); - svgContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)") - .set("padding", "var(--lumo-space-s)").set("background-color", "white") - .set("width", "100%").set("max-width", "450px").set("overflow", "hidden") - .set("display", "flex").set("align-items", "center") - .set("justify-content", "center"); + svgContainer.addClassNames("detail-card", "detail-card--asset"); + svgContainer.getStyle().set("width", "100%").set("max-width", "450px").set("overflow", + "hidden"); // Process SVG to make it responsive String responsiveSvg = makeResponsiveSvg(svgContent); @@ -1276,12 +1268,9 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has if (barcodeValue != null && !barcodeValue.isBlank()) { // Create a styled container for each barcode Div barcodeContainer = new Div(); - barcodeContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-s)") - .set("padding", "var(--lumo-space-s)").set("margin", "var(--lumo-space-xs) 0") - .set("background-color", "var(--lumo-contrast-5pct)") - .set("font-family", "monospace").set("font-size", "var(--lumo-font-size-s)") - .set("word-break", "break-all"); + barcodeContainer.addClassNames("detail-card", "detail-card--code"); + barcodeContainer.getStyle().set("margin", "var(--lumo-space-xs) 0").set("font-size", + "var(--lumo-font-size-s)"); Span barcodeSpan = new Span((i + 1) + ". " + barcodeValue); barcodeContainer.add(barcodeSpan); @@ -1315,10 +1304,8 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has for (Comment comment : comments) { Div commentContainer = new Div(); - commentContainer.getStyle().set("background-color", "#f5f5f5") - .set("border", "1px solid #ddd").set("border-radius", "4px").set("padding", "8px") - .set("margin", "4px 0").set("font-family", "monospace") - .set("white-space", "pre-wrap"); + commentContainer.addClassNames("detail-card", "detail-card--comment"); + commentContainer.getStyle().set("margin", "4px 0"); Span commentText = new Span(comment.getCommentText()); commentContainer.add(commentText); @@ -1338,48 +1325,28 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private Div createTaskCard(BaseTask task, String displayName) { Div taskCard = new Div(); - - // Card styling with fixed width - taskCard.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("margin", "var(--lumo-space-xs) 0").set("background-color", "var(--lumo-base-color)") - .set("cursor", "pointer").set("transition", "all 0.2s ease") - .set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)").set("display", "flex").set("align-items", "center") - .set("gap", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box"); - - // Hover effects - taskCard.getElement().addEventListener("mouseenter", e -> { - taskCard.getStyle().set("transform", "translateY(-2px)").set("box-shadow", "0 4px 12px rgba(0, 0, 0, 0.15)") - .set("border-color", "var(--lumo-primary-color-50pct)"); - }); - - taskCard.getElement().addEventListener("mouseleave", e -> { - taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") - .set("border-color", "var(--lumo-contrast-20pct)"); - }); + taskCard.addClassName("job-task-card"); + if (task.isCompleted()) { + taskCard.addClassName("completed"); + } // Task icon based on type Icon taskIcon = getTaskIcon(task); - taskIcon.getStyle().set("color", - task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-primary-color)"); + taskIcon.addClassName("job-task-icon"); // Task content VerticalLayout taskContent = new VerticalLayout(); taskContent.setPadding(false); taskContent.setSpacing(false); - taskContent.getStyle().set("flex-grow", "1"); + taskContent.addClassName("job-task-content"); // Task name with order number (display as 1-based instead of 0-based) String taskNameWithOrder = (task.getTaskOrder() != null ? (task.getTaskOrder() + 1) + ". " : "") + displayName; Span taskName = new Span(taskNameWithOrder); - taskName.getStyle().set("font-weight", "500").set("font-size", "var(--lumo-font-size-m)").set("color", - task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-body-text-color)"); + taskName.addClassName("job-task-name"); Span statusBadge = new Span(getTaskStatusLabel(task)); - statusBadge.getStyle().set("font-size", "var(--lumo-font-size-xs)").set("font-weight", "600") - .set("padding", "0.2rem 0.55rem").set("border-radius", "999px") - .set("background-color", task.isCompleted() ? "rgba(76, 175, 80, 0.15)" : "rgba(244, 67, 54, 0.12)") - .set("color", task.isCompleted() ? "var(--lumo-success-text-color)" : "var(--lumo-error-text-color)"); + statusBadge.addClassNames("job-task-status", task.isCompleted() ? "completed" : "open"); HorizontalLayout headerRow = new HorizontalLayout(taskName, statusBadge); headerRow.setWidthFull(); @@ -1390,24 +1357,18 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has // Task status/description Span taskDescription = new Span(getTaskDescription(task)); - taskDescription.getStyle().set("font-size", "var(--lumo-font-size-s)") - .set("color", "var(--lumo-secondary-text-color)").set("margin-top", "var(--lumo-space-xs)"); + taskDescription.addClassName("job-task-description"); taskContent.add(headerRow, taskDescription); // Status indicator Div statusIndicator = new Div(); - statusIndicator.getStyle().set("width", "8px").set("height", "8px").set("border-radius", "50%") - .set("background-color", task.isCompleted() ? "var(--lumo-success-color)" : "var(--lumo-error-color)"); + statusIndicator.addClassName("job-task-indicator"); taskCard.add(taskIcon, taskContent, statusIndicator); - // Click handler with hover state reset taskCard.addClickListener(event -> { showTaskDetailsDialog(task); - // Reset hover state after dialog interaction - taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") - .set("border-color", "var(--lumo-contrast-20pct)"); }); return taskCard; @@ -1480,17 +1441,11 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private Div createPhotoGallery(List photos) { Div galleryContainer = new Div(); - galleryContainer.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("background-color", "white").set("max-width", "600px").set("min-height", "500px") - .set("height", "500px").set("position", "relative").set("display", "flex").set("align-items", "center") - .set("justify-content", "center"); + galleryContainer.addClassName("job-photo-gallery"); if (photos.size() == 1) { // Single photo - no navigation needed Div photoContainer = createPhotoContainer(photos.get(0)); - photoContainer.getStyle().set("flex", "1").set("display", "flex").set("align-items", "center") - .set("justify-content", "center"); galleryContainer.add(photoContainer); } else { // Multiple photos - add navigation @@ -1498,11 +1453,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has // Photo counter Span photoCounter = new Span((currentIndex[0] + 1) + " / " + photos.size()); - photoCounter.getStyle().set("position", "absolute").set("top", "var(--lumo-space-s)") - .set("right", "var(--lumo-space-s)").set("background-color", "rgba(0, 0, 0, 0.6)") - .set("color", "white").set("padding", "var(--lumo-space-xs) var(--lumo-space-s)") - .set("border-radius", "var(--lumo-border-radius-s)").set("font-size", "var(--lumo-font-size-s)") - .set("z-index", "10"); + photoCounter.addClassName("job-photo-counter"); // Photo container Div photoContainer = createPhotoContainer(photos.get(0)); @@ -1513,16 +1464,12 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has // Previous button Button prevButton = new Button(new Icon(VaadinIcon.CHEVRON_LEFT)); prevButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON); - prevButton.getStyle().set("position", "absolute").set("left", "var(--lumo-space-s)").set("top", "50%") - .set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)") - .set("border-radius", "50%").set("z-index", "10"); + prevButton.addClassNames("job-photo-nav", "prev"); // Next button Button nextButton = new Button(new Icon(VaadinIcon.CHEVRON_RIGHT)); nextButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON); - nextButton.getStyle().set("position", "absolute").set("right", "var(--lumo-space-s)").set("top", "50%") - .set("transform", "translateY(-50%)").set("background-color", "rgba(255, 255, 255, 0.8)") - .set("border-radius", "50%").set("z-index", "10"); + nextButton.addClassNames("job-photo-nav", "next"); // Navigation logic prevButton.addClickListener(e -> { @@ -1557,8 +1504,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has private Div createPhotoContainer(String base64Photo) { Div photoContainer = new Div(); - photoContainer.getStyle().set("width", "100%").set("height", "100%").set("display", "flex") - .set("align-items", "center").set("justify-content", "center").set("overflow", "hidden"); + photoContainer.addClassName("job-photo-container"); // Create image element String imgSrc = base64Photo.startsWith("data:") ? base64Photo : "data:image/jpeg;base64," + base64Photo; @@ -1628,10 +1574,7 @@ public class JobSummaryView extends Main implements HasUrlParameter, Has } private void resetAllTaskCardHoverStates() { - for (Div taskCard : taskCards) { - taskCard.getStyle().set("transform", "translateY(0)").set("box-shadow", "0 1px 3px rgba(0, 0, 0, 0.1)") - .set("border-color", "var(--lumo-contrast-20pct)"); - } + // Hover states are handled purely in CSS. } private String getGoogleMapsApiKey() { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java index 3723f20..228c0eb 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/LoginView.java @@ -5,7 +5,10 @@ import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.login.LoginForm; import com.vaadin.flow.component.login.LoginI18n; import com.vaadin.flow.component.textfield.TextField; @@ -96,30 +99,48 @@ public class LoginView extends VerticalLayout implements BeforeEnterObserver, Af loginForm.addForgotPasswordListener(e -> UI.getCurrent().navigate(ForgotPasswordRequestView.class)); H1 title = new H1(getTranslation("login.votianlt")); - title.getStyle().set("color", "var(--lumo-primary-color)"); + title.addClassName("login-card-title"); Button registerButton = new Button(getTranslation("login.register"), e -> UI.getCurrent().navigate("register")); registerButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + registerButton.addClassName("login-register-button"); // Version display - will be set in @PostConstruct versionSpan = new Span(""); - versionSpan.getStyle().set("color", "var(--lumo-secondary-text-color)") - .set("font-size", "var(--lumo-font-size-s)").set("margin-top", "var(--lumo-space-m)"); + versionSpan.addClassName("login-version"); // Inline flash message box (hidden by default) - flashBox.getStyle().set("background", "var(--lumo-error-color-10pct)") - .set("color", "var(--lumo-error-text-color)").set("border", "1px solid var(--lumo-error-color)") - .set("border-radius", "var(--lumo-border-radius-m)").set("padding", "var(--lumo-space-m)") - .set("width", "100%").set("display", "none"); + flashBox.addClassName("inline-flash"); + flashBox.getStyle().set("display", "none"); VerticalLayout loginLayout = new VerticalLayout(); loginLayout.setAlignItems(FlexComponent.Alignment.CENTER); + loginLayout.addClassName("login-card"); loginLayout.add(flashBox, title, loginForm, twoFaField, verify2faButton, registerButton, versionSpan); - loginLayout.setMaxWidth("400px"); loginLayout.setPadding(true); - add(loginLayout); + Div loginHighlight = new Div(); + loginHighlight.addClassName("login-highlight"); + + Span loginEyebrow = new Span(getTranslation("login.title")); + loginEyebrow.addClassName("eyebrow-chip"); + + Icon introIcon = VaadinIcon.CUBES.create(); + introIcon.addClassName("login-highlight-icon"); + + H1 introTitle = new H1(getTranslation("login.votianlt")); + introTitle.addClassName("login-highlight-title"); + + Paragraph introText = new Paragraph(getTranslation("start.hero.description")); + introText.addClassName("login-highlight-text"); + + loginHighlight.add(loginEyebrow, introIcon, introTitle, introText); + + Div loginShell = new Div(loginHighlight, loginLayout); + loginShell.addClassName("login-shell"); + + add(loginShell); // Login-Schritt 1: Benutzername/Passwort loginForm.addLoginListener(e -> handlePasswordLogin(e.getUsername(), e.getPassword())); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java b/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java index ff100bb..386b323 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MessageDetailsView.java @@ -98,6 +98,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has this.messageService = messageService; this.messageBroadcaster = messageBroadcaster; this.eventPublisher = eventPublisher; + addClassName("message-hub-view"); // Set height to 100% to prevent page from growing beyond viewport setHeightFull(); @@ -109,6 +110,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has contentLayout.setWidthFull(); contentLayout.setHeightFull(); contentLayout.getStyle().set("overflow", "hidden"); + contentLayout.addClassName("message-thread-layout"); add(contentLayout); } @@ -172,8 +174,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has messagesContainer.setPadding(false); messagesContainer.setSpacing(false); messagesContainer.setWidthFull(); - messagesContainer.getStyle().set("background-color", "#f0f0f0").set("border-radius", "8px").set("padding", - "15px"); + messagesContainer.addClassName("message-thread"); // Wrap messages container in scroller for vertical scrolling messagesScroller = new Scroller(messagesContainer); @@ -274,7 +275,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has upload.setWidthFull(); Span helper = new Span("Unterstützte Formate: PNG, JPG, GIF, WebP (max. 32 MB)"); - helper.getStyle().set("font-size", "12px").set("color", "#666666"); + helper.addClassName("inline-caption"); Image preview = new Image(); preview.setAlt("Vorschau des ausgewählten Bildes"); @@ -388,14 +389,11 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has */ private Div createDateSeparator(LocalDate date) { Div separator = new Div(); - separator.getStyle().set("display", "flex").set("justify-content", "center").set("text-align", "center") - .set("margin", "20px 0"); separator.setWidthFull(); + separator.addClassName("message-date-separator"); Span dateLabel = new Span(DateTimeFormatUtil.formatDate(date)); - dateLabel.getStyle().set("background-color", "#d0d0d0").set("padding", "4px 10px").set("border-radius", "12px") - .set("font-size", "12px").set("font-weight", "500").set("color", "#333333") - .set("display", "inline-block"); + dateLabel.addClassName("message-date-chip"); separator.add(dateLabel); return separator; @@ -413,24 +411,20 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has // Container for the message (aligns left or right) Div messageWrapper = new Div(); String alignment = isServerMessage ? "right" : "left"; - - messageWrapper.getStyle().set("display", "flex") - .set("justify-content", isServerMessage ? "flex-end" : "flex-start").set("margin", "5px 0") - .set("width", "100%"); + messageWrapper.addClassNames("message-wrapper", isServerMessage ? "server" : "client"); // Message bubble Div bubble = new Div(); - bubble.getStyle().set("background-color", isServerMessage ? "#dcf8c6" : "#ffffff").set("padding", "10px 15px") - .set("border-radius", "18px").set("max-width", "70%").set("box-shadow", "0 1px 2px rgba(0,0,0,0.1)") - .set("word-wrap", "break-word").set("white-space", "pre-wrap").set("text-align", alignment); + bubble.addClassNames("message-bubble", isServerMessage ? "server" : "client"); + bubble.getStyle().set("text-align", alignment); // Message content component (text or media) Component contentComponent = createContentComponent(message, alignment); // Timestamp Span timeSpan = new Span(DateTimeFormatUtil.formatTime(timestamp)); - timeSpan.getStyle().set("font-size", "11px").set("color", isServerMessage ? "#666666" : "#999999") - .set("display", "block").set("text-align", alignment); + timeSpan.addClassName("message-bubble-time"); + timeSpan.getStyle().set("text-align", alignment); bubble.add(contentComponent, timeSpan); messageWrapper.add(bubble); @@ -450,15 +444,14 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has Div contentDiv = new Div(); String content = Optional.ofNullable(contentValue).filter(value -> !value.isBlank()).orElse("(kein Inhalt)"); contentDiv.setText(content); - contentDiv.getStyle().set("font-size", "14px").set("color", "#000000").set("margin-bottom", "5px") - .set("text-align", alignment); + contentDiv.addClassName("message-bubble-text"); + contentDiv.getStyle().set("text-align", alignment); return contentDiv; } private Component createImageContent(String base64Value, String alignment) { Div wrapper = new Div(); - wrapper.getStyle().set("margin-bottom", "5px").set("display", "flex").set("justify-content", - "right".equals(alignment) ? "flex-end" : "flex-start"); + wrapper.addClassNames("message-image-row", "right".equals(alignment) ? "server" : "client"); String trimmed = Optional.ofNullable(base64Value).map(String::trim).orElse(""); if (trimmed.isEmpty()) { @@ -473,8 +466,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has } Image image = new Image(dataUrl, "Nachrichtenbild"); - image.getStyle().set("max-width", "100%").set("border-radius", "12px").set("display", "block") - .set("max-height", "320px").set("height", "auto"); + image.addClassName("message-bubble-image"); image.getElement().setAttribute("loading", "lazy"); wrapper.add(image); @@ -687,8 +679,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has title.getStyle().set("margin", "0"); Span subtitle = new Span(conversationTitle); - subtitle.getStyle().set("color", "#666666"); - subtitle.getStyle().set("font-size", "14px"); + subtitle.addClassName("message-subtitle"); titleLayout.add(title, subtitle); @@ -696,6 +687,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has layout.setWidthFull(); layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER); layout.setSpacing(true); + layout.addClassName("message-thread-header"); return layout; } @@ -732,6 +724,7 @@ public class MessageDetailsView extends Main implements BeforeEnterObserver, Has layout.setAlignItems(FlexComponent.Alignment.END); layout.setSpacing(true); layout.expand(messageInput); + layout.addClassName("message-thread-input"); return layout; } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java index d1ee50a..3c324ce 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MessagesView.java @@ -4,12 +4,11 @@ import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.DetachEvent; import com.vaadin.flow.component.UI; import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.html.H2; import com.vaadin.flow.component.html.Main; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.notification.NotificationVariant; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.router.HasDynamicTitle; @@ -21,6 +20,7 @@ import de.assecutor.votianlt.model.Message; import de.assecutor.votianlt.model.MessageContentType; import de.assecutor.votianlt.model.MessageOrigin; import de.assecutor.votianlt.pages.service.AppUserService; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.service.MessageBroadcaster; import de.assecutor.votianlt.service.MessageService; import de.assecutor.votianlt.util.DateTimeFormatUtil; @@ -60,21 +60,29 @@ public class MessagesView extends Main implements HasDynamicTitle { this.messageService = messageService; this.appUserService = appUserService; this.messageBroadcaster = messageBroadcaster; + addClassNames("data-view", "message-hub-view"); // Create main layout VerticalLayout layout = new VerticalLayout(); layout.setPadding(true); layout.setSpacing(true); layout.setWidthFull(); + layout.addClassName("form-shell"); // Add title and action buttons - HorizontalLayout headerLayout = createHeaderLayout(); + ViewToolbar headerLayout = new ViewToolbar(getTranslation("messages.title")); // Create client grid clientGrid = createClientGrid(); + clientGrid.addClassName("data-grid"); + + Div gridPanel = new Div(clientGrid); + gridPanel.addClassNames("surface-panel", "data-grid-panel"); + gridPanel.setWidthFull(); + gridPanel.setHeight("680px"); // Add components to layout - layout.add(headerLayout, clientGrid); + layout.add(headerLayout, gridPanel); // Add layout to main view add(layout); @@ -83,18 +91,10 @@ public class MessagesView extends Main implements HasDynamicTitle { loadClientSummaries(); } - private HorizontalLayout createHeaderLayout() { - H2 title = new H2(getTranslation("messages.title")); - HorizontalLayout layout = new HorizontalLayout(title); - layout.setWidthFull(); - layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER); - return layout; - } - private Grid createClientGrid() { Grid grid = new Grid<>(ClientMessageSummary.class, false); grid.setWidthFull(); - grid.setHeight("600px"); + grid.setHeightFull(); // Add columns grid.addColumn(new ComponentRenderer<>(summary -> { diff --git a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java index 8d55423..bae3a4d 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/MyInvoicesView.java @@ -53,6 +53,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle { public MyInvoicesView(SystemInvoiceService systemInvoiceService) { this.systemInvoiceService = systemInvoiceService; + addClassName("data-view"); getStyle().set("max-width", "1100px"); getStyle().set("margin-left", "auto"); getStyle().set("margin-right", "auto"); @@ -74,8 +75,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle { private Component createTopCards() { // Container mit responsiven Spalten Div container = new Div(); - container.getStyle().set("display", "grid").set("grid-template-columns", "repeat(auto-fit, minmax(280px, 1fr))") - .set("gap", "var(--lumo-space-m)"); + container.addClassName("invoice-top-grid"); // Karte: Offene Rechnungen Paragraph hint = new Paragraph(getTranslation("myinvoices.hint.noopen")); @@ -159,9 +159,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle { H4 emptyTitle = new H4(getTranslation("myinvoices.empty.title")); Paragraph emptyDesc = new Paragraph(getTranslation("myinvoices.empty.desc")); emptyState.add(emptyTitle, emptyDesc); - emptyState.getStyle().set("text-align", "center").set("color", "var(--lumo-secondary-text-color)") - .set("padding", "var(--lumo-space-m)").set("border", "1px dashed var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-m)"); + emptyState.addClassName("empty-state-card"); updateEmptyStateVisibility(allRows); // Suche (einfacher Text-Filter über alle sichtbaren Felder) @@ -230,7 +228,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle { H3 h3 = new H3(title); h3.getStyle().set("margin", "0"); Div inner = new Div(content); - inner.getStyle().set("padding", "var(--lumo-space-s)"); + inner.addClassName("invoice-card-inner"); card.add(h3, inner); return card; } @@ -246,9 +244,7 @@ public class MyInvoicesView extends Main implements HasDynamicTitle { } private void styleCard(Div card) { - card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)") - .set("border-radius", "var(--lumo-border-radius-l)").set("padding", "var(--lumo-space-m)") - .set("background", "var(--lumo-base-color)").set("box-shadow", "0 1px 1px rgba(0,0,0,0.02)"); + card.addClassName("invoice-summary-card"); card.setWidthFull(); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java index a1adbb6..a2793fd 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/RegisterView.java @@ -61,6 +61,7 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); setPadding(true); setSpacing(false); + addClassName("form-page"); // Hauptcontainer für das Formular VerticalLayout formContainer = createFormContainer(); @@ -70,28 +71,18 @@ public class RegisterView extends VerticalLayout implements HasDynamicTitle { private VerticalLayout createFormContainer() { VerticalLayout container = new VerticalLayout(); container.setWidth("980px"); + container.setMaxWidth("100%"); container.setPadding(true); container.setSpacing(true); container.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); - - // Styling für den Container - container.getStyle().set("background-color", "var(--lumo-base-color)"); - container.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - container.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - container.getStyle().set("box-shadow", "var(--lumo-box-shadow-s)"); + container.addClassNames("form-shell", "form-card"); // Titel H1 title = new H1(getTranslation("register.title")); - title.getStyle().set("text-align", "center"); - title.getStyle().set("color", "var(--lumo-primary-color)"); - title.getStyle().set("margin-top", "0"); + title.addClassName("form-title"); H2 subtitle = new H2(getTranslation("register.subtitle")); - subtitle.getStyle().set("text-align", "center"); - subtitle.getStyle().set("color", "var(--lumo-secondary-text-color)"); - subtitle.getStyle().set("font-size", "var(--lumo-font-size-l)"); - subtitle.getStyle().set("font-weight", "normal"); - subtitle.getStyle().set("margin-bottom", "var(--lumo-space-l)"); + subtitle.addClassName("form-subtitle"); // Formularfelder emailField = new TextField(getTranslation("register.email")); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java index f9cd5ba..1c8bb5e 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowCustomersView.java @@ -2,14 +2,15 @@ package de.assecutor.votianlt.pages.view; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; -import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.HasDynamicTitle; import com.vaadin.flow.router.Route; import de.assecutor.votianlt.model.Customer; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.pages.service.CustomerService; import de.assecutor.votianlt.security.SecurityService; import jakarta.annotation.security.RolesAllowed; @@ -30,21 +31,14 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle setSizeFull(); setPadding(true); setSpacing(true); + addClassName("data-view"); - // Header with title and add button - HorizontalLayout header = new HorizontalLayout(); - header.setWidthFull(); - header.add(new H2(getTranslation("customers.title"))); Button addCustomerButton = new Button(getTranslation("customers.button.add"), new Icon(VaadinIcon.PLUS)); - header.add(addCustomerButton); - header.setJustifyContentMode(JustifyContentMode.BETWEEN); - header.setAlignItems(Alignment.CENTER); - add(header); + add(new ViewToolbar(getTranslation("customers.title"), addCustomerButton)); // Add hint text - var hintText = new com.vaadin.flow.component.html.Paragraph(getTranslation("customers.hint.click")); - hintText.getStyle().set("color", "var(--lumo-secondary-text-color)"); - hintText.getStyle().set("font-size", "var(--lumo-font-size-s)"); + Paragraph hintText = new Paragraph(getTranslation("customers.hint.click")); + hintText.addClassName("list-hint"); add(hintText); // Configure grid columns @@ -67,6 +61,7 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle grid.setMultiSort(true); grid.setSizeFull(); + grid.addClassName("data-grid"); // Make grid rows clickable grid.setSelectionMode(Grid.SelectionMode.SINGLE); @@ -80,7 +75,11 @@ public class ShowCustomersView extends VerticalLayout implements HasDynamicTitle } }); - add(grid); + Div gridPanel = new Div(grid); + gridPanel.addClassNames("surface-panel", "data-grid-panel"); + gridPanel.setSizeFull(); + + add(gridPanel); // Button action addCustomerButton.addClickListener(e -> getUI().ifPresent(ui -> ui.navigate("add-customer"))); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java index 3fa1015..7eb1a26 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/ShowJobsView.java @@ -7,7 +7,7 @@ import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.datepicker.DatePicker; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.html.Anchor; -import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.notification.Notification; @@ -21,6 +21,7 @@ import com.vaadin.flow.router.Route; import de.assecutor.votianlt.model.Job; import de.assecutor.votianlt.model.JobStatus; import de.assecutor.votianlt.messaging.MessagingPublisher; +import de.assecutor.votianlt.pages.base.ui.component.ViewToolbar; import de.assecutor.votianlt.util.DateTimeFormatUtil; import de.assecutor.votianlt.repository.CustomerInvoiceRepository; import de.assecutor.votianlt.repository.JobRepository; @@ -62,38 +63,39 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { setSizeFull(); setPadding(true); setSpacing(true); + addClassName("data-view"); startDate.setLabel(getTranslation("jobs.filter.startdate")); endDate.setLabel(getTranslation("jobs.filter.enddate")); searchField.setLabel(getTranslation("jobs.filter.search")); statusFilter.setLabel(getTranslation("jobs.filter.status")); - - H2 title = new H2(getTranslation("nav.jobs")); - add(title); + Button exportButton = new Button(getTranslation("jobs.button.csvexport")); + ViewToolbar toolbar = new ViewToolbar(getTranslation("nav.jobs"), exportButton); + add(toolbar); // Configure status filter statusFilter.setItems(getTranslation("jobs.status.all"), getTranslation("jobs.status.open"), getTranslation("jobs.status.done")); statusFilter.setValue(getTranslation("jobs.status.open")); - statusFilter.setWidth("150px"); // Configure search field searchField.setPlaceholder(getTranslation("jobs.filter.search.placeholder")); searchField.setClearButtonVisible(true); - searchField.setWidth("200px"); // Filterleiste mit Export-Button am rechten Rand Button applyFilter = new Button(getTranslation("jobs.filter.apply")); HorizontalLayout leftFilters = new HorizontalLayout(startDate, endDate, searchField, statusFilter, applyFilter); leftFilters.setAlignItems(Alignment.END); + leftFilters.getStyle().set("flex-wrap", "wrap"); HorizontalLayout filterBar = new HorizontalLayout(); filterBar.setWidthFull(); filterBar.add(leftFilters); - Button exportButton = new Button(getTranslation("jobs.button.csvexport")); filterBar.add(exportButton); filterBar.setJustifyContentMode(JustifyContentMode.BETWEEN); filterBar.setAlignItems(Alignment.END); + filterBar.getStyle().set("flex-wrap", "wrap"); + filterBar.addClassNames("surface-panel", "filter-panel"); add(filterBar); // Init default period: last 30 days @@ -186,6 +188,7 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { grid.setMultiSort(true); grid.setSizeFull(); + grid.addClassName("data-grid"); // Make grid rows clickable grid.setSelectionMode(Grid.SelectionMode.SINGLE); @@ -199,7 +202,11 @@ public class ShowJobsView extends VerticalLayout implements HasDynamicTitle { } }); - add(grid); + Div gridPanel = new Div(grid); + gridPanel.addClassNames("surface-panel", "data-grid-panel"); + gridPanel.setSizeFull(); + + add(gridPanel); loadData(); } diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java index de7e7b0..fc6e27b 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StartView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StartView.java @@ -33,9 +33,10 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha public StartView(SecurityService securityService, @Value("${app.version:unknown}") String appVersion) { this.securityService = securityService; this.appVersion = appVersion; + addClassName("landing-view"); setSizeFull(); setPadding(false); - setSpacing(false); + setSpacing(true); setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.STRETCH); // Header mit Navigation @@ -69,14 +70,11 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha header.setSpacing(true); header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); header.setJustifyContentMode(FlexComponent.JustifyContentMode.BETWEEN); - header.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); - header.getStyle().set("border-bottom", "1px solid var(--lumo-contrast-10pct)"); + header.addClassNames("page-shell", "surface-panel", "landing-header"); // Logo H1 logo = new H1("votian LT"); - logo.getStyle().set("color", "var(--lumo-primary-color)"); - logo.getStyle().set("margin", "0"); - logo.getStyle().set("font-weight", "bold"); + logo.addClassName("landing-logo"); // Navigation - abhängig vom Anmeldestatus Component navigation = securityService.isUserLoggedIn() ? createAuthenticatedNavigation() @@ -91,12 +89,15 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha HorizontalLayout navButtons = new HorizontalLayout(); navButtons.setSpacing(true); navButtons.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + navButtons.addClassName("landing-nav"); Button loginBtn = new Button(getTranslation("start.button.login"), event -> login()); loginBtn.addThemeVariants(ButtonVariant.LUMO_CONTRAST); + loginBtn.addClassName("landing-nav-button"); Button registerBtn = new Button(getTranslation("start.button.register"), event -> register()); registerBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + registerBtn.addClassName("landing-nav-button"); // Sprachauswahl Button @@ -114,6 +115,7 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha Button languageBtn = new Button(flag + " " + currentLang); languageBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + languageBtn.addClassName("landing-language-button"); ContextMenu languageMenu = new ContextMenu(); languageMenu.setOpenOnClick(true); @@ -186,10 +188,12 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha HorizontalLayout navLayout = new HorizontalLayout(); navLayout.setSpacing(true); navLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + navLayout.addClassName("landing-nav"); // Auftragserstellung Button Button createOrderBtn = new Button("Auftragserstellung", event -> UI.getCurrent().navigate("add_job")); createOrderBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + createOrderBtn.addClassName("landing-nav-button"); // Verwaltung ComboBox ComboBox managementCombo = new ComboBox<>(); @@ -254,28 +258,23 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha heroSection.setPadding(true); heroSection.setSpacing(true); heroSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - heroSection.getStyle().set("background", - "linear-gradient(135deg, var(--lumo-primary-color-10pct), var(--lumo-primary-color-50pct))"); - heroSection.getStyle().set("min-height", "400px"); heroSection.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); + heroSection.addClassNames("page-shell", "hero-panel", "landing-hero"); // Hero Image Placeholder Icon heroIcon = VaadinIcon.TRUCK.create(); heroIcon.setSize("120px"); - heroIcon.getStyle().set("color", "var(--lumo-primary-color)"); + heroIcon.addClassName("hero-panel-icon"); H1 heroTitle = new H1(getTranslation("start.title")); - heroTitle.getStyle().set("text-align", "center"); - heroTitle.getStyle().set("color", "var(--lumo-primary-text-color)"); - heroTitle.getStyle().set("margin-bottom", "var(--lumo-space-l)"); + heroTitle.addClassName("hero-panel-title"); Paragraph heroDescription = new Paragraph(getTranslation("start.hero.description")); - heroDescription.getStyle().set("text-align", "center"); - heroDescription.getStyle().set("max-width", "600px"); - heroDescription.getStyle().set("font-size", "var(--lumo-font-size-l)"); + heroDescription.addClassName("hero-panel-text"); Button ctaButton = new Button(getTranslation("cta.freetest"), event -> register()); ctaButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_LARGE); + ctaButton.addClassName("hero-cta"); heroSection.add(heroIcon, heroTitle, heroDescription, ctaButton); return heroSection; @@ -287,24 +286,18 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha systemSection.setPadding(true); systemSection.setSpacing(true); systemSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - systemSection.getStyle().set("background-color", "var(--lumo-base-color)"); + systemSection.addClassNames("page-shell", "surface-panel", "section-panel"); // Section Header H2 systemTitle = new H2(getTranslation("start.system.title")); - systemTitle.getStyle().set("color", "var(--lumo-primary-color)"); - systemTitle.getStyle().set("text-align", "center"); + systemTitle.addClassName("section-title"); Paragraph systemIntro = new Paragraph(getTranslation("start.system.intro")); - systemIntro.getStyle().set("text-align", "center"); - systemIntro.getStyle().set("max-width", "800px"); - systemIntro.getStyle().set("margin-bottom", "var(--lumo-space-xl)"); + systemIntro.addClassName("section-intro"); // Features Grid - HorizontalLayout featuresGrid = new HorizontalLayout(); - featuresGrid.setWidthFull(); - featuresGrid.setSpacing(true); - featuresGrid.setJustifyContentMode(FlexComponent.JustifyContentMode.CENTER); - featuresGrid.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.STRETCH); + Div featuresGrid = new Div(); + featuresGrid.addClassName("tile-grid"); // Feature Cards featuresGrid.add( @@ -324,24 +317,18 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha card.setSpacing(true); card.setPadding(true); card.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - card.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)"); - card.getStyle().set("border-radius", "var(--lumo-border-radius-m)"); - card.getStyle().set("background-color", "var(--lumo-base-color)"); - card.getStyle().set("box-shadow", "var(--lumo-box-shadow-xs)"); - card.setWidth("300px"); - card.setHeightFull(); + card.setWidthFull(); + card.addClassName("feature-card"); Icon icon = iconType.create(); icon.setSize("48px"); - icon.getStyle().set("color", "var(--lumo-primary-color)"); + icon.addClassName("feature-card-icon"); H3 cardTitle = new H3(title); - cardTitle.getStyle().set("text-align", "center"); - cardTitle.getStyle().set("margin", "var(--lumo-space-s) 0"); + cardTitle.addClassName("feature-card-title"); Paragraph cardDescription = new Paragraph(description); - cardDescription.getStyle().set("text-align", "center"); - cardDescription.getStyle().set("font-size", "var(--lumo-font-size-s)"); + cardDescription.addClassName("feature-card-description"); card.add(icon, cardTitle, cardDescription); return card; @@ -353,19 +340,17 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha appSection.setPadding(true); appSection.setSpacing(true); appSection.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - appSection.getStyle().set("background-color", "var(--lumo-contrast-5pct)"); + appSection.addClassNames("page-shell", "surface-panel", "app-overview-panel"); H2 appTitle = new H2(getTranslation("start.app.title")); - appTitle.getStyle().set("color", "var(--lumo-primary-color)"); - appTitle.getStyle().set("text-align", "center"); + appTitle.addClassName("section-title"); Paragraph appDescription = new Paragraph(getTranslation("start.app.description")); - appDescription.getStyle().set("text-align", "center"); - appDescription.getStyle().set("max-width", "800px"); + appDescription.addClassName("section-intro"); Icon appIcon = VaadinIcon.MOBILE.create(); appIcon.setSize("80px"); - appIcon.getStyle().set("color", "var(--lumo-primary-color)"); + appIcon.addClassName("feature-card-icon"); appSection.add(appTitle, appDescription, appIcon); return appSection; @@ -377,18 +362,17 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha footer.setPadding(true); footer.setSpacing(true); footer.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); - footer.getStyle().set("background-color", "var(--lumo-contrast-10pct)"); - footer.getStyle().set("border-top", "1px solid var(--lumo-contrast-20pct)"); + footer.addClassNames("page-shell", "surface-panel", "footer-panel"); // Company Info H3 companyTitle = new H3(getTranslation("start.imprint.title")); - companyTitle.getStyle().set("color", "var(--lumo-primary-color)"); - companyTitle.getStyle().set("text-align", "center"); + companyTitle.addClassName("section-title"); VerticalLayout companyInfo = new VerticalLayout(); companyInfo.setSpacing(false); companyInfo.setPadding(false); companyInfo.setDefaultHorizontalComponentAlignment(FlexComponent.Alignment.CENTER); + companyInfo.addClassName("footer-details"); companyInfo.add(new Paragraph(getTranslation("start.imprint.company")), new Paragraph(getTranslation("start.imprint.address")), @@ -397,20 +381,14 @@ public class StartView extends VerticalLayout implements BeforeEnterObserver, Ha // Call to Action Paragraph ctaText = new Paragraph(getTranslation("start.cta.text")); - ctaText.getStyle().set("text-align", "center"); - ctaText.getStyle().set("font-weight", "bold"); - ctaText.getStyle().set("color", "var(--lumo-primary-color)"); - ctaText.getStyle().set("margin-top", "var(--lumo-space-l)"); + ctaText.addClassName("footer-cta"); Paragraph slogan = new Paragraph(getTranslation("start.slogan")); - slogan.getStyle().set("text-align", "center"); - slogan.getStyle().set("font-style", "italic"); - slogan.getStyle().set("color", "var(--lumo-primary-color)"); + slogan.addClassName("footer-slogan"); // Versionsnummer Span versionSpan = new Span("Version " + appVersion); - versionSpan.getStyle().set("color", "var(--lumo-secondary-text-color)") - .set("font-size", "var(--lumo-font-size-s)").set("margin-top", "var(--lumo-space-l)"); + versionSpan.addClassName("footer-version"); footer.add(companyTitle, companyInfo, ctaText, slogan, versionSpan); return footer; diff --git a/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java b/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java index 8d6cdcf..76a31fd 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/StatisticsView.java @@ -53,6 +53,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { setSizeFull(); setPadding(false); setSpacing(false); + addClassName("statistics-chat-view"); // Header HorizontalLayout header = createHeader(); @@ -64,11 +65,13 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { chatContainer.setPadding(true); chatContainer.setSpacing(true); chatContainer.getStyle().set("padding-bottom", "20px"); + chatContainer.addClassName("statistics-chat-container"); Scroller scroller = new Scroller(chatContainer); scroller.setSizeFull(); scroller.setScrollDirection(Scroller.ScrollDirection.VERTICAL); scroller.getStyle().set("background", "var(--lumo-contrast-5pct)"); + scroller.addClassName("statistics-scroller"); add(scroller); setFlexGrow(1, scroller); @@ -83,8 +86,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { header.setWidthFull(); header.setPadding(true); header.setAlignItems(FlexComponent.Alignment.CENTER); - header.getStyle().set("background", "var(--lumo-base-color)").set("border-bottom", - "1px solid var(--lumo-contrast-10pct)"); + header.addClassName("statistics-header"); Icon aiIcon = VaadinIcon.MAGIC.create(); aiIcon.getStyle().set("color", "var(--lumo-primary-color)"); @@ -106,8 +108,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { inputArea.setPadding(true); inputArea.setSpacing(true); inputArea.setAlignItems(FlexComponent.Alignment.CENTER); - inputArea.getStyle().set("background", "var(--lumo-base-color)").set("border-top", - "1px solid var(--lumo-contrast-10pct)"); + inputArea.addClassName("statistics-input-panel"); Button sendButton = new Button(VaadinIcon.PAPERPLANE.create()); sendButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -124,6 +125,7 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { HorizontalLayout quickActions = new HorizontalLayout(jobCountBtn, revenueBtn, trendBtn); quickActions.setSpacing(true); + quickActions.addClassName("statistics-quick-actions"); VerticalLayout inputWrapper = new VerticalLayout(); inputWrapper.setPadding(false); @@ -193,17 +195,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { private void addUserMessage(String message) { Div messageDiv = new Div(); - messageDiv.addClassName("chat-message"); - messageDiv.addClassName("user-message"); - messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-end").set("margin-bottom", - "var(--lumo-space-m)"); + messageDiv.addClassNames("chat-message", "user-message"); Div bubble = new Div(); - bubble.getStyle().set("background", "var(--lumo-primary-color)") - .set("color", "var(--lumo-primary-contrast-color)") - .set("padding", "var(--lumo-space-s) var(--lumo-space-m)") - .set("border-radius", "var(--lumo-border-radius-l)").set("max-width", "70%") - .set("word-wrap", "break-word"); + bubble.addClassNames("chat-bubble", "chat-bubble--user"); Paragraph text = new Paragraph(message); text.getStyle().set("margin", "0"); @@ -219,16 +214,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { private void addAiResponse(AiStatisticsService.StatisticsResponse response) { Div messageDiv = new Div(); - messageDiv.addClassName("chat-message"); - messageDiv.addClassName("ai-message"); - messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-start").set("margin-bottom", - "var(--lumo-space-m)"); + messageDiv.addClassNames("chat-message", "ai-message"); Div bubble = new Div(); - bubble.getStyle().set("background", "var(--lumo-base-color)") - .set("border", "1px solid var(--lumo-contrast-10pct)").set("padding", "var(--lumo-space-m)") - .set("border-radius", "var(--lumo-border-radius-l)").set("max-width", "85%") - .set("box-shadow", "var(--lumo-box-shadow-xs)"); + bubble.addClassNames("chat-bubble", "chat-bubble--ai"); // AI Icon HorizontalLayout header = new HorizontalLayout(); @@ -283,14 +272,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { private void addErrorMessage(String message) { Div messageDiv = new Div(); - messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-start").set("margin-bottom", - "var(--lumo-space-m)"); + messageDiv.addClassNames("chat-message", "error-message"); Div bubble = new Div(); - bubble.getStyle().set("background", "var(--lumo-error-color-10pct)") - .set("border", "1px solid var(--lumo-error-color)") - .set("padding", "var(--lumo-space-s) var(--lumo-space-m)") - .set("border-radius", "var(--lumo-border-radius-l)").set("max-width", "70%"); + bubble.addClassNames("chat-bubble", "chat-bubble--error"); Icon errorIcon = VaadinIcon.EXCLAMATION_CIRCLE.create(); errorIcon.setSize("16px"); @@ -310,14 +295,10 @@ public class StatisticsView extends VerticalLayout implements HasDynamicTitle { private Div createLoadingMessage() { Div messageDiv = new Div(); - messageDiv.getStyle().set("display", "flex").set("justify-content", "flex-start").set("margin-bottom", - "var(--lumo-space-m)"); + messageDiv.addClassNames("chat-message", "loading-message"); Div bubble = new Div(); - bubble.getStyle().set("background", "var(--lumo-base-color)") - .set("border", "1px solid var(--lumo-contrast-10pct)") - .set("padding", "var(--lumo-space-s) var(--lumo-space-m)") - .set("border-radius", "var(--lumo-border-radius-l)"); + bubble.addClassNames("chat-bubble", "chat-bubble--loading"); Span dots = new Span(getTranslation("statistics.loading")); dots.getStyle().set("color", "var(--lumo-secondary-text-color)").set("font-style", "italic"); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java index 70119eb..bc961c7 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/UserMessagesView.java @@ -49,12 +49,14 @@ public class UserMessagesView extends Main implements HasUrlParameter, H public UserMessagesView(AppUserService appUserService, MessageService messageService) { this.appUserService = appUserService; this.messageService = messageService; + addClassName("message-hub-view"); // Create main layout contentLayout = new VerticalLayout(); contentLayout.setPadding(true); contentLayout.setSpacing(true); contentLayout.setWidthFull(); + contentLayout.addClassName("form-shell"); add(contentLayout); } @@ -104,6 +106,7 @@ public class UserMessagesView extends Main implements HasUrlParameter, H layout.setWidthFull(); layout.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER); layout.setSpacing(true); + layout.addClassName("message-thread-header"); return layout; } @@ -112,12 +115,11 @@ public class UserMessagesView extends Main implements HasUrlParameter, H VerticalLayout section = new VerticalLayout(); section.setPadding(true); section.setSpacing(true); - section.getStyle().set("border", "1px solid #e0e0e0"); - section.getStyle().set("border-radius", "8px"); section.setWidthFull(); - section.getStyle().set("margin-right", "20px"); + section.addClassName("message-section"); H3 title = new H3(getTranslation("usermessages.general.title")); + title.addClassName("message-section-title"); section.add(title); List sortedMessages = new ArrayList<>(); @@ -145,12 +147,11 @@ public class UserMessagesView extends Main implements HasUrlParameter, H VerticalLayout section = new VerticalLayout(); section.setPadding(true); section.setSpacing(true); - section.getStyle().set("border", "1px solid #e0e0e0"); - section.getStyle().set("border-radius", "8px"); section.setWidthFull(); - section.getStyle().set("margin-right", "20px"); + section.addClassName("message-section"); H3 title = new H3(getTranslation("usermessages.job.title")); + title.addClassName("message-section-title"); section.add(title); if (jobMessages == null || jobMessages.isEmpty()) { @@ -191,40 +192,21 @@ public class UserMessagesView extends Main implements HasUrlParameter, H int messageCount, int unreadCount, String conversationId) { Div card = new Div(); card.setWidthFull(); - card.getStyle().set("padding", "15px"); - card.getStyle().set("border", "1px solid #e0e0e0"); - card.getStyle().set("border-radius", "8px"); card.getStyle().set("cursor", "pointer"); - card.getStyle().set("background-color", "#ffffff"); - card.getStyle().set("margin-bottom", "10px"); - card.getStyle().set("max-width", "97.5%"); card.addClassName("message-card"); - // Hover effect - card.getElement().addEventListener("mouseenter", e -> { - card.getStyle().set("background-color", "#f5f5f5"); - }); - card.getElement().addEventListener("mouseleave", e -> { - card.getStyle().set("background-color", "#ffffff"); - }); - // Title row with unread indicator HorizontalLayout titleRow = new HorizontalLayout(); titleRow.setWidthFull(); titleRow.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER); + titleRow.setSpacing(true); Span titleSpan = new Span(conversationTitle); - titleSpan.getStyle().set("font-weight", "bold"); - titleSpan.getStyle().set("font-size", "16px"); + titleSpan.addClassName("message-section-title"); if (unreadCount > 0) { Span unreadBadge = new Span(String.valueOf(unreadCount)); - unreadBadge.getStyle().set("background-color", "var(--lumo-primary-color)"); - unreadBadge.getStyle().set("color", "white"); - unreadBadge.getStyle().set("border-radius", "50%"); - unreadBadge.getStyle().set("padding", "2px 8px"); - unreadBadge.getStyle().set("font-size", "12px"); - unreadBadge.getStyle().set("font-weight", "bold"); + unreadBadge.addClassName("message-badge"); titleRow.add(titleSpan, unreadBadge); } else { titleRow.add(titleSpan); @@ -235,20 +217,16 @@ public class UserMessagesView extends Main implements HasUrlParameter, H // Preview text Span preview = new Span(Optional.ofNullable(lastMessagePreview).filter(s -> !s.isBlank()) .orElse(getTranslation("usermessages.preview.empty"))); - preview.getStyle().set("color", "#666666"); - preview.getStyle().set("font-size", "14px"); + preview.addClassName("message-preview"); // Metadata row HorizontalLayout metaRow = new HorizontalLayout(); metaRow.setWidthFull(); + metaRow.addClassName("message-meta"); Span timeSpan = new Span(lastMessageTime != null ? DateTimeFormatUtil.formatDateTime(lastMessageTime) : "-"); - timeSpan.getStyle().set("color", "#999999"); - timeSpan.getStyle().set("font-size", "12px"); Span countSpan = new Span(getTranslation("usermessages.message.count", messageCount)); - countSpan.getStyle().set("color", "#999999"); - countSpan.getStyle().set("font-size", "12px"); metaRow.add(timeSpan, countSpan); metaRow.expand(timeSpan); diff --git a/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java b/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java index 8b764df..46a3b76 100644 --- a/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java +++ b/src/main/java/de/assecutor/votianlt/pages/view/VerwaltungView.java @@ -21,6 +21,7 @@ public class VerwaltungView extends Main implements HasDynamicTitle { setSizeFull(); addClassNames(LumoUtility.BoxSizing.BORDER, LumoUtility.Display.FLEX, LumoUtility.FlexDirection.COLUMN, LumoUtility.Padding.MEDIUM, LumoUtility.Gap.SMALL); + addClassName("data-view"); add(new ViewToolbar(getTranslation("verwaltung.title"))); @@ -37,6 +38,7 @@ public class VerwaltungView extends Main implements HasDynamicTitle { content.setDefaultHorizontalComponentAlignment(VerticalLayout.Alignment.CENTER); content.setJustifyContentMode(VerticalLayout.JustifyContentMode.CENTER); content.setSizeFull(); + content.addClassNames("surface-panel", "section-panel"); add(content); }