Erweiterungen
This commit is contained in:
441
HANDBUCH.md
Normal file
441
HANDBUCH.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# VotianLT - Benutzerhandbuch
|
||||
|
||||
**Version:** 1.0
|
||||
**Stand:** Februar 2026
|
||||
**Anwendung:** VotianLT - Ihr digitaler Transportpartner
|
||||
|
||||
---
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Einleitung](#1-einleitung)
|
||||
2. [Registrierung und Anmeldung](#2-registrierung-und-anmeldung)
|
||||
3. [Dashboard](#3-dashboard)
|
||||
4. [Auftragsverwaltung](#4-auftragsverwaltung)
|
||||
5. [Kundenverwaltung](#5-kundenverwaltung)
|
||||
6. [App-Nutzer verwalten](#6-app-nutzer-verwalten)
|
||||
7. [Nachrichten](#7-nachrichten)
|
||||
8. [Rechnungen](#8-rechnungen)
|
||||
9. [KI-Statistiken](#9-ki-statistiken)
|
||||
10. [Profil bearbeiten](#10-profil-bearbeiten)
|
||||
11. [Administration](#11-administration)
|
||||
|
||||
---
|
||||
|
||||
## 1. Einleitung
|
||||
|
||||
VotianLT ist eine webbasierte Anwendung zur digitalen Verwaltung von Transportaufträgen. Die Software richtet sich an Solo-Selbstständige und Kleinunternehmer im Transportgewerbe und bietet folgende Kernfunktionen:
|
||||
|
||||
- **Auftragserstellung und -verwaltung** mit vollständiger Abwicklung von der Erstellung bis zur Fertigstellung
|
||||
- **Digitale Auftragsbearbeitung** über eine mobile App für Fahrer und Mitarbeiter im Außendienst
|
||||
- **Echtzeit-Kommunikation** zwischen Disponent und App-Nutzern per Chat
|
||||
- **Rechnungserstellung** für abgeschlossene Aufträge
|
||||
- **KI-gestützte Statistiken** zur Auswertung von Geschäftsdaten
|
||||
- **GPS-Tracking** zur Nachverfolgung von Lieferungen in Echtzeit
|
||||
|
||||
---
|
||||
|
||||
## 2. Registrierung und Anmeldung
|
||||
|
||||
### 2.1 Neues Konto registrieren
|
||||
|
||||
1. Öffnen Sie die Startseite und klicken Sie auf **"Jetzt kostenlos testen"** oder **"Registrieren"**.
|
||||
2. Füllen Sie das Registrierungsformular aus:
|
||||
- **Firmenname** (Pflichtfeld)
|
||||
- **E-Mail-Adresse** (wird als Benutzername verwendet)
|
||||
- **Passwort** und **Passwort bestätigen** (mindestens 6 Zeichen)
|
||||
- **Vorname** und **Nachname**
|
||||
- **Telefonnummer**
|
||||
- **Anschrift** (Straße, Hausnummer, PLZ, Ort)
|
||||
3. Klicken Sie auf **"Registrieren"**. Ein 6-stelliger Bestätigungscode wird an Ihre E-Mail-Adresse gesendet.
|
||||
4. Geben Sie den Code im angezeigten Feld ein und klicken Sie auf **"Code prüfen und registrieren"**.
|
||||
5. Nach erfolgreicher Registrierung werden Sie zur Anmeldeseite weitergeleitet.
|
||||
|
||||
> **Hinweis:** Der Bestätigungscode ist 10 Minuten gültig. Über den Button **"Code erneut senden"** können Sie nach 60 Sekunden einen neuen Code anfordern.
|
||||
|
||||
### 2.2 Anmelden
|
||||
|
||||
1. Geben Sie auf der Anmeldeseite Ihre **E-Mail-Adresse** und Ihr **Passwort** ein.
|
||||
2. Klicken Sie auf **"Anmelden"**.
|
||||
3. Falls die Zwei-Faktor-Authentifizierung (2FA) aktiviert ist, wird ein 6-stelliger Code an Ihre E-Mail gesendet. Geben Sie diesen im angezeigten Feld ein und klicken Sie auf **"Code prüfen"**.
|
||||
|
||||
### 2.3 Passwort zurücksetzen
|
||||
|
||||
1. Klicken Sie auf der Anmeldeseite auf **"Passwort vergessen?"**.
|
||||
2. Geben Sie Ihre registrierte E-Mail-Adresse ein und klicken Sie auf **"E-Mail senden"**.
|
||||
3. Sie erhalten eine E-Mail mit einem Link zum Zurücksetzen. Klicken Sie auf den Link.
|
||||
4. Vergeben Sie ein neues Passwort und bestätigen Sie es. Klicken Sie auf **"Passwort speichern"**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard
|
||||
|
||||
Nach der Anmeldung gelangen Sie zum Dashboard. Es zeigt eine Willkommensnachricht und eine Übersicht der wichtigsten Funktionen:
|
||||
|
||||
- **Einrichtungsassistent** - Hilfe beim Einstieg in die Anwendung
|
||||
- **Kunden- und Auftragsverwaltung** - Schnellzugriff auf die Verwaltungsbereiche
|
||||
- **Auftragserstellung** - Direkter Link zur Erstellung neuer Aufträge
|
||||
|
||||
Die **Hauptnavigation** am linken Rand bietet Zugriff auf alle Bereiche der Anwendung.
|
||||
|
||||
---
|
||||
|
||||
## 4. Auftragsverwaltung
|
||||
|
||||
### 4.1 Aufträge anzeigen
|
||||
|
||||
Öffnen Sie den Bereich **"Aufträge"** über die Navigation oder den Pfad `/jobs`.
|
||||
|
||||
**Filtermöglichkeiten:**
|
||||
- **Zeitraum**: Wählen Sie Start- und Enddatum (Standard: letzte 30 Tage)
|
||||
- **Auftragsnummer**: Suchen Sie nach einer bestimmten Auftragsnummer
|
||||
- **Status**: Filtern Sie nach "Alle", "Offen" oder "Erledigt"
|
||||
- *Offen* umfasst: Erstellt, In Bearbeitung, Abholung geplant, Abgeholt, In Zustellung
|
||||
- *Erledigt* umfasst: Zugestellt, Abgeschlossen, Storniert
|
||||
|
||||
Klicken Sie auf **"Anwenden"**, um die Filter zu übernehmen.
|
||||
|
||||
**Verfügbare Aktionen pro Auftrag:**
|
||||
|
||||
| Symbol | Aktion | Sichtbar wenn |
|
||||
|--------|--------|---------------|
|
||||
| Häkchen | Auftrag manuell abschließen | Nicht-digitale Aufträge, die noch nicht abgeschlossen sind |
|
||||
| Dollar | Rechnung erstellen | Auftrag ist abgeschlossen |
|
||||
| Papierkorb | Auftrag löschen | Auftrag ist nicht abgeschlossen/storniert/zugestellt |
|
||||
|
||||
**CSV-Export:** Klicken Sie auf **"CSV Export"**, um die aktuell gefilterte Auftragsliste als CSV-Datei herunterzuladen.
|
||||
|
||||
Klicken Sie auf eine Zeile, um die **Auftragszusammenfassung** zu öffnen.
|
||||
|
||||
### 4.2 Neuen Auftrag anlegen
|
||||
|
||||
Navigieren Sie zu **"Auftragserstellung"** oder dem Pfad `/add_job`. Das Formular ist in fünf Tabs unterteilt:
|
||||
|
||||
#### Tab 1: Auftraggeber & Adressen
|
||||
|
||||
1. **Auftraggeber wählen**: Wählen Sie aus der Dropdown-Liste einen bestehenden Kunden. Die Abholadresse wird automatisch mit den Kundendaten vorbefüllt.
|
||||
2. **Abholadresse**: Füllen Sie alle Pflichtfelder aus (Firma, Anrede, Vorname, Nachname, Telefon, Straße, Hausnummer, PLZ, Ort). Optional können Sie die Adresse für zukünftige Aufträge speichern.
|
||||
3. **Zustelladresse**: Geben Sie die Lieferadresse ein. Firmennamen werden per Autovervollständigung vorgeschlagen.
|
||||
4. Mit dem Button **"Vorbelegte Adressfelder leeren"** können Sie alle vorausgefüllten Felder zurücksetzen.
|
||||
|
||||
> **Hinweis:** Beim Wechsel zum nächsten Tab werden die Adressen automatisch validiert und eine Routenberechnung (Entfernung und Dauer) durchgeführt. Bei ungültigen Adressen wird ein Hinweisdialog angezeigt.
|
||||
|
||||
#### Tab 2: Termine & Verarbeitung
|
||||
|
||||
1. **Abholtermin**: Wählen Sie Datum und Uhrzeit für die Abholung.
|
||||
2. **Zustelltermin**: Wählen Sie Datum und Uhrzeit für die Zustellung.
|
||||
3. **Digitale Abwicklung per App**: Aktivieren Sie diese Option, wenn der Auftrag über die mobile App abgewickelt werden soll.
|
||||
4. **App-Nutzer**: Wählen Sie den zuständigen App-Nutzer aus (nur sichtbar bei digitaler Abwicklung).
|
||||
|
||||
#### Tab 3: Ladung
|
||||
|
||||
1. Klicken Sie auf **"Ladung hinzufügen"**, um eine neue Ladungsposition anzulegen.
|
||||
2. Für jede Position geben Sie ein:
|
||||
- **Beschreibung** der Ladung
|
||||
- **Menge**
|
||||
- **Maße** (Länge, Breite, Höhe in cm)
|
||||
- **Gewicht** in kg
|
||||
3. Über das Papierkorb-Symbol können Sie einzelne Positionen entfernen.
|
||||
|
||||
#### Tab 4: Aufgaben (nur bei digitaler Abwicklung)
|
||||
|
||||
Aufgaben definieren, welche Schritte der App-Nutzer bei der Ausführung des Auftrags erledigen muss.
|
||||
|
||||
**Verfügbare Aufgabentypen:**
|
||||
|
||||
| Typ | Beschreibung |
|
||||
|-----|-------------|
|
||||
| Bestätigung | Der App-Nutzer bestätigt eine Aktion (z.B. "Ware übernommen") |
|
||||
| Unterschrift | Einholen einer digitalen Unterschrift |
|
||||
| Foto | Aufnahme eines Fotos (z.B. Liefernachweis) |
|
||||
| Barcode | Scannen eines Barcodes |
|
||||
| Checkliste | Abarbeiten einer Aufgabenliste |
|
||||
| Kommentar | Freitextfeld für Anmerkungen |
|
||||
|
||||
1. Optional: Wählen Sie eine **Aufgabenvorlage** aus der Dropdown-Liste, um vordefinierte Aufgaben zu laden.
|
||||
2. Klicken Sie auf **"Aufgabe hinzufügen"** und wählen Sie den gewünschten Typ.
|
||||
3. Geben Sie eine Beschreibung für die Aufgabe ein.
|
||||
4. Die Reihenfolge der Aufgaben kann per **Drag-and-Drop** angepasst werden.
|
||||
|
||||
#### Tab 5: Leistungen und Preis
|
||||
|
||||
1. **Pflichtleistungen** werden automatisch geladen.
|
||||
2. Über die Dropdown-Liste können Sie weitere **Leistungen hinzufügen**.
|
||||
3. Die Berechnung von **Netto**, **MwSt.** und **Brutto** erfolgt automatisch.
|
||||
4. Die **Routeninformationen** (Entfernung und Fahrtdauer) werden aus der Adressvalidierung angezeigt.
|
||||
5. Im Feld **Bemerkung** können Sie zusätzliche Anmerkungen zum Auftrag hinterlegen.
|
||||
|
||||
Klicken Sie abschließend auf **"Auftrag anlegen"**, um den Auftrag zu erstellen. Bei digitaler Abwicklung wird der Auftrag automatisch an den ausgewählten App-Nutzer übermittelt.
|
||||
|
||||
### 4.3 Auftragszusammenfassung
|
||||
|
||||
Die Zusammenfassung zeigt alle Details eines Auftrags auf einen Blick:
|
||||
|
||||
- **Abholinformationen**: Adresse, Ansprechpartner, Termin
|
||||
- **Zustellinformationen**: Adresse, Ansprechpartner, Termin
|
||||
- **Aufgaben**: Übersicht aller Aufgaben mit Erledigungsstatus. Abgeschlossene Aufgaben zeigen das Ergebnis (Foto, Barcode-Wert, Unterschrift-Vorschau etc.)
|
||||
- **Ladung**: Liste aller Ladungspositionen mit Maßen und Gewicht
|
||||
- **Auftragsinformationen**: Preis, Bemerkung, Verarbeitungsart, zugewiesener App-Nutzer
|
||||
- **Routenkarte**: Google Maps mit der Route von Abhol- zu Zustelladresse. Bei aktiver digitaler Abwicklung wird die aktuelle **GPS-Position des App-Nutzers** in Echtzeit angezeigt.
|
||||
|
||||
**Verfügbare Aktionen:**
|
||||
- **"Nachricht senden"**: Öffnet den Chat mit dem zugewiesenen App-Nutzer im Kontext dieses Auftrags
|
||||
- **"Job Historie"**: Zeigt den vollständigen Änderungsverlauf des Auftrags
|
||||
- **"Auftrag manuell abschließen"**: Schließt nicht-digitale Aufträge manuell ab (nach Bestätigung)
|
||||
|
||||
### 4.4 Auftragsverlauf (Job Historie)
|
||||
|
||||
Die Job Historie zeigt eine chronologische Zeitleiste aller Ereignisse eines Auftrags:
|
||||
|
||||
- **Statusänderungen** (z.B. "Erstellt", "In Bearbeitung", "Zugestellt")
|
||||
- **Aufgabenabschlüsse** mit zugehörigen Daten (Fotos, Barcodes, Unterschriften)
|
||||
- **Änderungen** an Auftragsdaten
|
||||
|
||||
Jeder Eintrag zeigt den **Zeitstempel**, die **Art der Änderung** und ggf. **zugehörige Daten** (z.B. Foto-Vorschau).
|
||||
|
||||
---
|
||||
|
||||
## 5. Kundenverwaltung
|
||||
|
||||
### 5.1 Kunden anzeigen
|
||||
|
||||
Navigieren Sie zu **"Kunden"** (`/customers`). Die Kundenliste zeigt alle Ihre Kunden mit folgenden Spalten:
|
||||
|
||||
- Firma, Name, E-Mail, Telefon, Straße, Ort
|
||||
|
||||
Die Liste ist **sortierbar** (Klick auf Spaltenüberschrift). Klicken Sie auf einen Eintrag, um den Kunden zu bearbeiten.
|
||||
|
||||
### 5.2 Neuen Kunden anlegen
|
||||
|
||||
1. Klicken Sie auf **"Kunde hinzufügen"** oder navigieren Sie zu `/add-customer`.
|
||||
2. Füllen Sie das Formular aus:
|
||||
- **Firmenname** (Pflichtfeld)
|
||||
- **Anrede** (Herr / Frau / Divers)
|
||||
- **Vorname** und **Nachname** (Pflichtfelder)
|
||||
- **Telefon** (Pflichtfeld)
|
||||
- **Fax** (optional)
|
||||
- **E-Mail** (Pflichtfeld, wird auf Gültigkeit geprüft)
|
||||
- **Straße** und **Hausnummer** (Pflichtfelder)
|
||||
- **Adresszusatz** (optional)
|
||||
- **PLZ** und **Ort** (Pflichtfelder)
|
||||
3. Klicken Sie auf **"Kunden anlegen"**.
|
||||
|
||||
### 5.3 Kunden bearbeiten
|
||||
|
||||
1. Klicken Sie in der Kundenliste auf den gewünschten Kunden.
|
||||
2. Ändern Sie die gewünschten Felder.
|
||||
3. Klicken Sie auf **"Speichern"**, um die Änderungen zu übernehmen.
|
||||
4. Über **"Löschen"** können Sie den Kunden nach Bestätigung endgültig entfernen.
|
||||
|
||||
---
|
||||
|
||||
## 6. App-Nutzer verwalten
|
||||
|
||||
App-Nutzer sind Fahrer oder Mitarbeiter, die die mobile VotianLT-App verwenden, um Aufträge digital abzuwickeln.
|
||||
|
||||
### 6.1 App-Nutzer anzeigen
|
||||
|
||||
Navigieren Sie zu **"App-Nutzer"** (`/app-user`). Die Liste zeigt alle Ihre App-Nutzer mit:
|
||||
|
||||
- Bezeichnung (Kennzeichen/Code), Vorname, Nachname, Telefon, App-Code, E-Mail
|
||||
|
||||
### 6.2 Neuen App-Nutzer anlegen
|
||||
|
||||
1. Klicken Sie auf **"Neuen App-Nutzer anlegen"**.
|
||||
2. Füllen Sie das Formular aus:
|
||||
- **Bezeichnung** (z.B. Fahrzeugkennzeichen im Format "HH H 000", Pflichtfeld)
|
||||
- **Vorname** und **Nachname** (Pflichtfelder)
|
||||
- **Telefon** (Pflichtfeld)
|
||||
- **E-Mail** (Pflichtfeld, muss eindeutig sein)
|
||||
- **Passwort** und **Passwort bestätigen** (mindestens 6 Zeichen)
|
||||
3. Klicken Sie auf **"App-Nutzer anlegen"**.
|
||||
|
||||
> **Hinweis:** Der App-Nutzer meldet sich in der mobilen App mit seiner E-Mail-Adresse und dem vergebenen Passwort an.
|
||||
|
||||
### 6.3 App-Nutzer bearbeiten
|
||||
|
||||
1. Klicken Sie in der Liste auf den gewünschten App-Nutzer.
|
||||
2. Ändern Sie die gewünschten Felder.
|
||||
3. **Passwort ändern**: Lassen Sie die Passwortfelder leer, wenn das Passwort nicht geändert werden soll. Füllen Sie beide Felder aus, um ein neues Passwort zu vergeben.
|
||||
4. Klicken Sie auf **"Speichern"**.
|
||||
5. Über **"Löschen"** können Sie den App-Nutzer nach Bestätigung entfernen.
|
||||
|
||||
---
|
||||
|
||||
## 7. Nachrichten
|
||||
|
||||
Das Nachrichtensystem ermöglicht die Echtzeit-Kommunikation zwischen Ihnen (Disponent) und Ihren App-Nutzern.
|
||||
|
||||
### 7.1 Nachrichtenübersicht
|
||||
|
||||
Navigieren Sie zu **"Nachrichten"** (`/messages`). Die Übersicht zeigt alle Ihre App-Nutzer als Kontakte mit:
|
||||
|
||||
- **Status-Anzeige** (farbiger Punkt bei ungelesenen Nachrichten)
|
||||
- **Name** und **E-Mail** des App-Nutzers
|
||||
- **Gesamtanzahl** der Nachrichten
|
||||
- **Ungelesene Nachrichten** (fett hervorgehoben)
|
||||
- **Letzte Nachricht** (Vorschau, Datum und Uhrzeit)
|
||||
|
||||
> **Hinweis:** Die Übersicht aktualisiert sich automatisch. Bei eingehenden Nachrichten ertönt ein **Benachrichtigungston** und es wird eine **Browser-Benachrichtigung** angezeigt (sofern Benachrichtigungen im Browser erlaubt sind).
|
||||
|
||||
### 7.2 Konversationen eines App-Nutzers
|
||||
|
||||
Klicken Sie auf einen App-Nutzer, um dessen Konversationen zu sehen:
|
||||
|
||||
- **Allgemeine Nachrichten**: Ein allgemeiner Chat ohne Auftragsbezug
|
||||
- **Nachrichten zu Aufträgen**: Separate Konversationen für jeden Auftrag (gruppiert nach Auftragsnummer)
|
||||
|
||||
Jede Konversation zeigt eine Vorschau, den Zeitpunkt der letzten Nachricht und die Anzahl ungelesener Nachrichten.
|
||||
|
||||
### 7.3 Chat / Nachrichtenverlauf
|
||||
|
||||
Öffnen Sie eine Konversation, um den Chatverlauf zu sehen. Die Darstellung ähnelt bekannten Messenger-Apps:
|
||||
|
||||
- **Ihre Nachrichten** (vom Disponenten): rechts, grüner Hintergrund
|
||||
- **Nachrichten des App-Nutzers**: links, weißer Hintergrund
|
||||
- **Datumstrenner** zwischen verschiedenen Tagen
|
||||
- **Zeitstempel** bei jeder Nachricht
|
||||
|
||||
**Nachricht senden:**
|
||||
1. Geben Sie Ihre Nachricht im Textfeld am unteren Rand ein.
|
||||
2. Klicken Sie auf **"Senden"**.
|
||||
|
||||
**Bild senden:**
|
||||
1. Klicken Sie auf das **Büroklammer-Symbol** neben dem Textfeld.
|
||||
2. Wählen Sie ein Bild aus (PNG, JPEG, GIF oder WebP, max. 32 MB).
|
||||
3. Eine Vorschau wird angezeigt. Bestätigen Sie den Versand.
|
||||
|
||||
> **Hinweis:** Neue Nachrichten erscheinen in Echtzeit. Die Ansicht scrollt automatisch zur neuesten Nachricht. Geöffnete Nachrichten werden automatisch als gelesen markiert.
|
||||
|
||||
---
|
||||
|
||||
## 8. Rechnungen
|
||||
|
||||
### 8.1 Rechnung für einen Auftrag erstellen
|
||||
|
||||
1. Navigieren Sie zur **Auftragsliste** (`/jobs`).
|
||||
2. Filtern Sie nach Status **"Erledigt"**.
|
||||
3. Klicken Sie auf das **Dollar-Symbol** neben dem gewünschten Auftrag.
|
||||
4. Das Rechnungsformular zeigt:
|
||||
- **Auftragsdetails** (Abhol- und Zustelladresse, Termine)
|
||||
- **Leistungsdaten**: Geben Sie Kilometer und Zeitaufwand ein
|
||||
- **Leistungen**: Wählen Sie die abzurechnenden Leistungen aus Ihrem Leistungskatalog
|
||||
- **Zusammenfassung**: Automatische Berechnung von Netto, MwSt. und Brutto
|
||||
5. Klicken Sie auf **"Rechnung erstellen"**.
|
||||
|
||||
### 8.2 Meine Rechnungen
|
||||
|
||||
Unter **"Meine Rechnungen"** (`/my-invoices`) finden Sie eine Übersicht aller erstellten Rechnungen:
|
||||
|
||||
- **Statusanzeige**: Offen (gelb), Bezahlt (grün), Überfällig (rot)
|
||||
- **Rechnungsnummer**, **Datum**, **Betrag**
|
||||
- **Suchfunktion** zum Filtern nach Rechnungsnummer, Status oder Betrag
|
||||
|
||||
Klicken Sie auf eine Rechnung, um die **PDF-Version** herunterzuladen.
|
||||
|
||||
---
|
||||
|
||||
## 9. KI-Statistiken
|
||||
|
||||
Der KI-Statistik-Assistent (`/statistics`) bietet eine interaktive Auswertung Ihrer Geschäftsdaten per Chat-Schnittstelle.
|
||||
|
||||
### Verwendung
|
||||
|
||||
1. Navigieren Sie zu **"KI-Statistiken"**.
|
||||
2. Verwenden Sie die **Schnellaktionen** für häufige Abfragen:
|
||||
- **"Aufträge zählen"**: Zeigt die Gesamtanzahl Ihrer Aufträge
|
||||
- **"Umsatz"**: Berechnet Ihren Gesamtumsatz
|
||||
- **"Monatstrend"**: Zeigt die monatliche Entwicklung als Diagramm
|
||||
3. Oder geben Sie eine **freie Frage** in das Textfeld ein, z.B.:
|
||||
- "Wie viele Aufträge hatte ich im Januar?"
|
||||
- "Zeige mir den Umsatz der letzten 6 Monate als Diagramm"
|
||||
- "Welcher Kunde hat die meisten Aufträge?"
|
||||
|
||||
Der Assistent analysiert Ihre Daten und antwortet mit **Text** und/oder **interaktiven Diagrammen** (Balken-, Linien-, Kreis- und weitere Diagrammtypen).
|
||||
|
||||
---
|
||||
|
||||
## 10. Profil bearbeiten
|
||||
|
||||
Unter **"Profil bearbeiten"** (`/edit-profile`) können Sie Ihre persönlichen Daten und Rechnungseinstellungen verwalten. Die Seite ist in drei Tabs unterteilt:
|
||||
|
||||
### Tab 1: Persönliche Daten / Kontakt
|
||||
|
||||
- **Firmendaten**: Firmenname (Pflicht), Firmenzusatz
|
||||
- **Persönliche Daten**: Vorname, Nachname
|
||||
- **Kontakt**: Telefon (Pflicht), Fax, Mobil, E-Mail (Pflicht)
|
||||
- **Anschrift**: Straße, Hausnummer (Pflicht), Adresszusatz, PLZ, Ort (Pflicht)
|
||||
- **Abweichende Rechnungsadresse**: Separate Adressfelder für die Rechnungsstellung
|
||||
- **Digitale Abwicklung per App vorausgewählt**: Wenn aktiviert, wird bei neuen Aufträgen die digitale Abwicklung standardmäßig vorausgewählt
|
||||
- **Zwei-Faktor-Authentifizierung (2FA)**: Aktivieren/deaktivieren Sie die zusätzliche Sicherheitsabfrage beim Login
|
||||
|
||||
### Tab 2: Rechnungsdaten
|
||||
|
||||
- **Rechnungspräfix** (z.B. "VLT" - wird der Rechnungsnummer vorangestellt)
|
||||
- **USt-IdNr.** und **Steuernummer**
|
||||
- **Bankverbindung**: Bankname und IBAN
|
||||
- **Steuersatz** (z.B. 19%)
|
||||
- **Einleitungstext**: Freitext, der auf Rechnungen als Einleitung erscheint
|
||||
- **AGB/Zahlungsbedingungen**: Text für die Rechnungsfußzeile
|
||||
- **Logo hochladen**: Laden Sie Ihr Firmenlogo hoch, das auf Rechnungen angezeigt wird
|
||||
- **Abrechnung aktiviert**: Aktivieren Sie die Rechnungsfunktion
|
||||
|
||||
### Tab 3: Leistungen / Preisliste
|
||||
|
||||
Verwalten Sie Ihren **Leistungskatalog**, der bei der Auftragserstellung und Rechnungsstellung verwendet wird:
|
||||
|
||||
1. Klicken Sie auf **"Leistung hinzufügen"**, um eine neue Leistung anzulegen.
|
||||
2. Wählen Sie die **Leistungsbezeichnung** aus oder geben Sie eine neue ein.
|
||||
3. Geben Sie den **Preis** und den **MwSt.-Satz** ein.
|
||||
4. Über das Papierkorb-Symbol können Sie einzelne Leistungen entfernen.
|
||||
|
||||
Rechts neben dem Formular wird eine **Live-Vorschau** Ihrer Rechnungsvorlage angezeigt, die sich bei Änderungen automatisch aktualisiert.
|
||||
|
||||
---
|
||||
|
||||
## 11. Administration
|
||||
|
||||
Die folgenden Bereiche stehen nur Benutzern mit Administratorrechten zur Verfügung.
|
||||
|
||||
### 11.1 Admin Dashboard
|
||||
|
||||
Das Admin Dashboard (`/admin-dashboard`) bietet eine Gesamtübersicht des Systems:
|
||||
|
||||
- **System-Übersicht**: Anzahl Aufträge, Benutzer, App-Benutzer
|
||||
- **Job-Statistiken**: Offene, in Bearbeitung befindliche und abgeschlossene Aufträge sowie Frachtgüter
|
||||
- **Aufgaben-Statistiken**: Gesamtanzahl, abgeschlossene und offene Aufgaben mit Erfolgsquote
|
||||
- **Benutzer-Aktivität**: Anzahl Fotos, Barcodes, Unterschriften und Kommentare
|
||||
- **System-Status**: Datenbankverbindung, WebSocket-Status, Anwendungsstatus, Speicherverbrauch
|
||||
|
||||
### 11.2 Preis-Tabelle
|
||||
|
||||
Unter **"Preis-Tabelle"** (`/admin-price-table`) können Administratoren die Systempreise konfigurieren:
|
||||
|
||||
- **Monatliche Grundpauschale**
|
||||
- **App-Nutzungslizenz**
|
||||
- **Umsatzbeteiligung** (in Prozent)
|
||||
|
||||
### 11.3 Rechnungsgenerator
|
||||
|
||||
Der Rechnungsgenerator (`/invoice-generator`) ist ein visueller Editor zum Erstellen von Rechnungsvorlagen:
|
||||
|
||||
- **Drag-and-Drop** von Vorlagenblöcken (Textfeld, Überschrift, Datum, Kundeninfo, Firmeninfo, Betrag, Linie, Bild) auf eine Arbeitsfläche
|
||||
- **Eigenschaften-Panel** zur Bearbeitung des ausgewählten Elements
|
||||
- **Funktionen**: Speichern, Laden, PDF-Vorschau, Template-Verwaltung
|
||||
|
||||
### 11.4 PDF-Test
|
||||
|
||||
Unter `/pdf-test` können Administratoren die PDF-Generierung testen:
|
||||
|
||||
- **System-Rechnung generieren**: Erzeugt eine PDF aus der System-Rechnungsvorlage
|
||||
- **Kunden-Rechnung generieren**: Erzeugt eine PDF aus der Kunden-Rechnungsvorlage
|
||||
|
||||
---
|
||||
|
||||
## Impressum
|
||||
|
||||
Das Impressum ist unter `/impressum` einsehbar und enthält die gesetzlich vorgeschriebenen Angaben des Betreibers.
|
||||
|
||||
---
|
||||
|
||||
*Dieses Handbuch beschreibt den Funktionsumfang von VotianLT. Bei Fragen oder Problemen wenden Sie sich bitte an den Support.*
|
||||
BIN
HANDBUCH.pdf
Normal file
BIN
HANDBUCH.pdf
Normal file
Binary file not shown.
@@ -12,6 +12,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Document(collection = "jobs")
|
||||
@@ -141,6 +142,14 @@ public class Job {
|
||||
@Field("time_in_15min_units")
|
||||
private Integer timeIn15MinUnits;
|
||||
|
||||
// Service-IDs für die Rechnung
|
||||
@Field("service_ids")
|
||||
private List<String> serviceIds;
|
||||
|
||||
// Streckeninformation für die Rechnung (in km)
|
||||
@Field("route_distance_km")
|
||||
private Double routeDistanceKm;
|
||||
|
||||
/**
|
||||
* Returns the ObjectId as string for JSON serialization. This ensures that the
|
||||
* job id is returned as a string when jobs are retrieved via API.
|
||||
|
||||
@@ -77,7 +77,6 @@ public final class AdminLayout extends AppLayout {
|
||||
|
||||
// Only admin-specific menu items
|
||||
SideNavItem dashboard = new SideNavItem("Dashboard", "admin-dashboard", new Icon(VaadinIcon.DASHBOARD));
|
||||
SideNavItem pdfTest = new SideNavItem("PDF Test", "pdf-test", new Icon(VaadinIcon.FILE_TEXT_O));
|
||||
SideNavItem invoiceGenerator = new SideNavItem("Rechnungsgenerator", "invoice-generator",
|
||||
new Icon(VaadinIcon.FILE_PROCESS));
|
||||
SideNavItem priceTable = new SideNavItem("Preis-Tabelle", "admin-price-table", new Icon(VaadinIcon.COG));
|
||||
@@ -89,7 +88,6 @@ public final class AdminLayout extends AppLayout {
|
||||
// Icon(VaadinIcon.FILE_TEXT));
|
||||
|
||||
nav.addItem(dashboard);
|
||||
nav.addItem(pdfTest);
|
||||
nav.addItem(invoiceGenerator);
|
||||
nav.addItem(priceTable);
|
||||
// nav.addItem(systemSettings);
|
||||
|
||||
@@ -123,8 +123,6 @@ public class AddressValidationService {
|
||||
String locationType = geometry.path("location_type").asText();
|
||||
boolean isPrecise = "ROOFTOP".equals(locationType) || "RANGE_INTERPOLATED".equals(locationType);
|
||||
|
||||
// Adresskomponenten prüfen
|
||||
boolean hasStreetNumber = false;
|
||||
boolean hasPostalCode = false;
|
||||
|
||||
JsonNode addressComponents = firstResult.path("address_components");
|
||||
@@ -133,7 +131,6 @@ public class AddressValidationService {
|
||||
for (JsonNode type : types) {
|
||||
String typeStr = type.asText();
|
||||
if ("street_number".equals(typeStr)) {
|
||||
hasStreetNumber = true;
|
||||
} else if ("postal_code".equals(typeStr)) {
|
||||
hasPostalCode = true;
|
||||
}
|
||||
|
||||
@@ -675,11 +675,22 @@ public class AddJobView extends Main {
|
||||
return "";
|
||||
}).setHeader("Berechnung").setSortable(true);
|
||||
servicesGrid.addColumn(service -> {
|
||||
if (service.getEffectivePrice() != null) {
|
||||
return service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €";
|
||||
// Get route distance for distance-based calculations
|
||||
Double routeDistance = (routeCalculationResult != null && routeCalculationResult.isValid())
|
||||
? routeCalculationResult.getDistanceKm()
|
||||
: null;
|
||||
BigDecimal price = calculateServicePrice(service, routeDistance);
|
||||
if (price.compareTo(BigDecimal.ZERO) > 0) {
|
||||
return price.setScale(2, RoundingMode.HALF_UP) + " €";
|
||||
}
|
||||
return "";
|
||||
}).setHeader("Preis").setSortable(true);
|
||||
// Show price info if no route calculated yet
|
||||
if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE && routeDistance == null) {
|
||||
return service.getPricePerKilometer().setScale(2, RoundingMode.HALF_UP) + " €/km (Route fehlt)";
|
||||
}
|
||||
return service.getEffectivePrice() != null
|
||||
? service.getEffectivePrice().setScale(2, RoundingMode.HALF_UP) + " €"
|
||||
: "";
|
||||
}).setHeader("Preis").setSortable(false);
|
||||
servicesGrid.addColumn(service -> {
|
||||
if (service.getVatRate() != null) {
|
||||
return service.getVatRate().multiply(new BigDecimal("100")).setScale(0, RoundingMode.HALF_UP) + " %";
|
||||
@@ -687,6 +698,10 @@ public class AddJobView extends Main {
|
||||
return "";
|
||||
}).setHeader("MwSt").setSortable(true);
|
||||
servicesGrid.addComponentColumn(service -> {
|
||||
// Verbindliche Leistungen können nicht gelöscht werden
|
||||
if (service.isMandatory()) {
|
||||
return new Span(""); // Leeres Element statt Löschen-Button
|
||||
}
|
||||
Button removeButton = new Button(new Icon(VaadinIcon.TRASH));
|
||||
removeButton.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY,
|
||||
ButtonVariant.LUMO_SMALL);
|
||||
@@ -822,8 +837,13 @@ public class AddJobView extends Main {
|
||||
BigDecimal vatTotal = BigDecimal.ZERO;
|
||||
BigDecimal grossTotal = BigDecimal.ZERO;
|
||||
|
||||
// Get route distance for distance-based calculations
|
||||
Double routeDistance = (routeCalculationResult != null && routeCalculationResult.isValid())
|
||||
? routeCalculationResult.getDistanceKm()
|
||||
: null;
|
||||
|
||||
for (Service service : selectedServices) {
|
||||
BigDecimal price = service.getEffectivePrice() != null ? service.getEffectivePrice() : BigDecimal.ZERO;
|
||||
BigDecimal price = calculateServicePrice(service, routeDistance);
|
||||
BigDecimal vatRate = service.getVatRate() != null ? service.getVatRate() : BigDecimal.ZERO;
|
||||
|
||||
netTotal = netTotal.add(price);
|
||||
@@ -837,6 +857,35 @@ public class AddJobView extends Main {
|
||||
grossTotalLabel.setText(grossTotal.setScale(2, RoundingMode.HALF_UP).toString().replace(".", ",") + " €");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the actual price for a service based on its calculation basis and
|
||||
* route distance (for distance-based services).
|
||||
*/
|
||||
private BigDecimal calculateServicePrice(Service service, Double routeDistance) {
|
||||
if (service.getCalculationBasis() == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
switch (service.getCalculationBasis()) {
|
||||
case FLAT_RATE:
|
||||
return service.getPrice() != null ? service.getPrice() : BigDecimal.ZERO;
|
||||
|
||||
case DISTANCE:
|
||||
if (service.getPricePerKilometer() != null && routeDistance != null && routeDistance > 0) {
|
||||
return service.getPricePerKilometer().multiply(BigDecimal.valueOf(routeDistance));
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
|
||||
case TIME:
|
||||
// For time-based services, we would need time units
|
||||
// For now, return the price per 15 minutes as base value
|
||||
return service.getPricePer15Minutes() != null ? service.getPricePer15Minutes() : BigDecimal.ZERO;
|
||||
|
||||
default:
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private VerticalLayout createPickupSection() {
|
||||
VerticalLayout section = new VerticalLayout();
|
||||
section.setSpacing(true);
|
||||
@@ -1407,8 +1456,13 @@ public class AddJobView extends Main {
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
job.setPrice(totalPrice);
|
||||
|
||||
// Store selected service IDs in job (optional - if Job has serviceIds field)
|
||||
// job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
||||
// Store selected service IDs in job for invoice creation
|
||||
job.setServiceIds(selectedServices.stream().map(Service::getId).toList());
|
||||
|
||||
// Store route distance in job for invoice creation
|
||||
if (routeCalculationResult != null && routeCalculationResult.isValid()) {
|
||||
job.setRouteDistanceKm(routeCalculationResult.getDistanceKm());
|
||||
}
|
||||
|
||||
// Validate all required fields using the binder
|
||||
if (binder.writeBeanIfValid(job)) {
|
||||
@@ -2802,10 +2856,6 @@ public class AddJobView extends Main {
|
||||
return field.getValue() != null ? field.getValue().trim() : "";
|
||||
}
|
||||
|
||||
private String getComboValueOrEmpty(ComboBox<String> field) {
|
||||
return field.getValue() != null ? field.getValue().trim() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt den Adressvalidierungsdialog an. Die Prüfung erfolgt im Hintergrund und
|
||||
* der Dialog wird aktualisiert, sobald die Ergebnisse vorliegen.
|
||||
@@ -3034,6 +3084,12 @@ public class AddJobView extends Main {
|
||||
routeDistanceLabel.setText(String.format("%.1f km", routeCalculationResult.getDistanceKm()));
|
||||
routeDurationLabel.setText(routeCalculationResult.getFormattedDurationLong());
|
||||
routeInfoBox.setVisible(true);
|
||||
|
||||
// Update price summary and grid with new route distance
|
||||
updatePriceSummary();
|
||||
if (servicesGrid != null) {
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
}
|
||||
} else {
|
||||
routeInfoBox.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -43,10 +43,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
|
||||
private Job currentJob;
|
||||
private List<ServiceRow> gridRows = new ArrayList<>();
|
||||
private List<Service> allUserServices;
|
||||
private Grid<ServiceRow> servicesGrid;
|
||||
private IntegerField kilometersField;
|
||||
private IntegerField timeField;
|
||||
private Div servicesSection;
|
||||
|
||||
/**
|
||||
@@ -88,6 +85,20 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
setSpacing(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Services, die beim Job-Erstellen ausgewählt wurden.
|
||||
*/
|
||||
private void loadSelectedServicesFromJob() {
|
||||
if (currentJob.getServiceIds() != null && !currentJob.getServiceIds().isEmpty()) {
|
||||
gridRows.clear();
|
||||
for (String serviceId : currentJob.getServiceIds()) {
|
||||
serviceRepository.findById(serviceId).ifPresent(service -> {
|
||||
gridRows.add(new ServiceRow(service));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setParameter(BeforeEvent event, String jobIdHex) {
|
||||
try {
|
||||
@@ -116,13 +127,18 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
H2 title = new H2("Rechnung erstellen für Auftrag " + currentJob.getJobNumber());
|
||||
add(title);
|
||||
|
||||
// Load previously selected services from job
|
||||
loadSelectedServicesFromJob();
|
||||
|
||||
// Job Details Section
|
||||
Div jobDetailsSection = createJobDetailsSection();
|
||||
add(jobDetailsSection);
|
||||
|
||||
// Performance Data Section
|
||||
Div performanceDataSection = createPerformanceDataSection();
|
||||
add(performanceDataSection);
|
||||
// Route Information Section (if available)
|
||||
if (currentJob.getRouteDistanceKm() != null && currentJob.getRouteDistanceKm() > 0) {
|
||||
Div routeInfoSection = createRouteInfoSection();
|
||||
add(routeInfoSection);
|
||||
}
|
||||
|
||||
// Services Selection Section
|
||||
Div servicesSection = createServicesSelectionSection();
|
||||
@@ -164,46 +180,28 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
return section;
|
||||
}
|
||||
|
||||
private Div createPerformanceDataSection() {
|
||||
private Div createRouteInfoSection() {
|
||||
Div section = new Div();
|
||||
section.getStyle().set("border", "1px solid var(--lumo-contrast-20pct)")
|
||||
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("margin-bottom", "var(--lumo-space-m)").set("width", "100%").set("box-sizing", "border-box")
|
||||
.set("background-color", "var(--lumo-primary-color-10pct)");
|
||||
|
||||
H3 sectionTitle = new H3("Leistungsdaten");
|
||||
H3 sectionTitle = new H3("Streckeninformation");
|
||||
sectionTitle.getStyle().set("color", "var(--lumo-primary-text-color)");
|
||||
section.add(sectionTitle);
|
||||
|
||||
VerticalLayout performanceLayout = new VerticalLayout();
|
||||
performanceLayout.setSpacing(true);
|
||||
performanceLayout.setWidthFull();
|
||||
VerticalLayout routeInfo = new VerticalLayout();
|
||||
routeInfo.setSpacing(true);
|
||||
routeInfo.setWidthFull();
|
||||
|
||||
// Kilometers field
|
||||
HorizontalLayout kilometersLayout = new HorizontalLayout();
|
||||
kilometersLayout.setWidthFull();
|
||||
Span kilometersLabel = new Span("Gefahrene Kilometer:");
|
||||
kilometersLabel.getStyle().set("width", "200px");
|
||||
kilometersField = new IntegerField();
|
||||
kilometersField.setWidth("150px");
|
||||
kilometersField.setMin(0);
|
||||
kilometersField.setValue(currentJob.getKilometersDriven() != null ? currentJob.getKilometersDriven() : 0);
|
||||
kilometersField.addValueChangeListener(e -> updateSummarySection());
|
||||
kilometersLayout.add(kilometersLabel, kilometersField);
|
||||
performanceLayout.add(kilometersLayout);
|
||||
Double distance = currentJob.getRouteDistanceKm();
|
||||
if (distance != null) {
|
||||
routeInfo.add(new HorizontalLayout(new Span("Berechnete Entfernung:"),
|
||||
new Span(String.format("%.1f km", distance))));
|
||||
}
|
||||
|
||||
// Time field (in 15-minute units)
|
||||
HorizontalLayout timeLayout = new HorizontalLayout();
|
||||
timeLayout.setWidthFull();
|
||||
Span timeLabel = new Span("Arbeitszeit (15-Minuten-Einheiten):");
|
||||
timeLabel.getStyle().set("width", "200px");
|
||||
timeField = new IntegerField();
|
||||
timeField.setWidth("150px");
|
||||
timeField.setMin(0);
|
||||
timeField.setValue(currentJob.getTimeIn15MinUnits() != null ? currentJob.getTimeIn15MinUnits() : 0);
|
||||
timeField.addValueChangeListener(e -> updateSummarySection());
|
||||
timeLayout.add(timeLabel, timeField);
|
||||
performanceLayout.add(timeLayout);
|
||||
|
||||
section.add(performanceLayout);
|
||||
section.add(routeInfo);
|
||||
return section;
|
||||
}
|
||||
|
||||
@@ -213,46 +211,23 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
.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");
|
||||
|
||||
H3 sectionTitle = new H3("Leistungen auswählen");
|
||||
H3 sectionTitle = new H3("Leistungen");
|
||||
servicesSection.add(sectionTitle);
|
||||
|
||||
// Load services for current user (only once)
|
||||
if (allUserServices == null) {
|
||||
String currentUserId = securityService.getCurrentUserId().toHexString();
|
||||
allUserServices = serviceRepository.findByUserId(currentUserId);
|
||||
}
|
||||
|
||||
// Initialize with 2 empty rows if gridRows is empty
|
||||
if (gridRows.isEmpty()) {
|
||||
gridRows.add(new ServiceRow());
|
||||
gridRows.add(new ServiceRow());
|
||||
}
|
||||
|
||||
// Create grid with editable rows
|
||||
// Create grid with read-only rows
|
||||
servicesGrid = new Grid<>();
|
||||
servicesGrid.setWidthFull();
|
||||
servicesGrid.setAllRowsVisible(true);
|
||||
|
||||
// Service selection column (ComboBox)
|
||||
servicesGrid.addComponentColumn(row -> {
|
||||
ComboBox<Service> serviceCombo = new ComboBox<>();
|
||||
serviceCombo.setItems(allUserServices);
|
||||
serviceCombo.setItemLabelGenerator(Service::getName);
|
||||
serviceCombo.setPlaceholder("Leistung auswählen...");
|
||||
serviceCombo.setWidthFull();
|
||||
serviceCombo.setValue(row.getService());
|
||||
|
||||
serviceCombo.addValueChangeListener(event -> {
|
||||
row.setService(event.getValue());
|
||||
// Refresh the grid to show updated calculation basis and price
|
||||
servicesGrid.getDataProvider().refreshItem(row);
|
||||
updateSummarySection();
|
||||
});
|
||||
|
||||
return serviceCombo;
|
||||
// Service name column (read-only)
|
||||
servicesGrid.addColumn(row -> {
|
||||
if (row.getService() != null) {
|
||||
return row.getService().getName();
|
||||
}
|
||||
return "";
|
||||
}).setHeader("Leistung").setAutoWidth(true).setFlexGrow(2);
|
||||
|
||||
// Calculation basis column
|
||||
// Calculation basis column (read-only)
|
||||
servicesGrid.addColumn(row -> {
|
||||
if (row.getService() != null && row.getService().getCalculationBasis() != null) {
|
||||
return switch (row.getService().getCalculationBasis()) {
|
||||
@@ -264,7 +239,7 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
return "";
|
||||
}).setHeader("Berechnungsgrundlage").setAutoWidth(true).setFlexGrow(1);
|
||||
|
||||
// Price column
|
||||
// Price column (read-only)
|
||||
servicesGrid.addColumn(row -> {
|
||||
if (row.getService() != null) {
|
||||
BigDecimal price = calculateServicePrice(row.getService());
|
||||
@@ -278,15 +253,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
servicesGrid.setItems(gridRows);
|
||||
servicesSection.add(servicesGrid);
|
||||
|
||||
// Add button to add new row
|
||||
Button addButton = new Button("Leistung hinzufügen", e -> {
|
||||
ServiceRow newRow = new ServiceRow();
|
||||
gridRows.add(newRow);
|
||||
servicesGrid.getDataProvider().refreshAll();
|
||||
});
|
||||
addButton.getStyle().set("margin-top", "var(--lumo-space-m)");
|
||||
servicesSection.add(addButton);
|
||||
|
||||
return servicesSection;
|
||||
}
|
||||
|
||||
@@ -342,13 +308,12 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
||||
return service.getPrice();
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
||||
&& service.getPricePerKilometer() != null && kilometersField != null
|
||||
&& kilometersField.getValue() != null) {
|
||||
BigDecimal kilometers = new BigDecimal(kilometersField.getValue());
|
||||
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
|
||||
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
|
||||
return service.getPricePerKilometer().multiply(kilometers);
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
||||
&& service.getPricePer15Minutes() != null && timeField != null && timeField.getValue() != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(timeField.getValue());
|
||||
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
|
||||
return service.getPricePer15Minutes().multiply(timeUnits);
|
||||
}
|
||||
|
||||
@@ -362,14 +327,13 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
if (service.getCalculationBasis() == Service.CalculationBasis.FLAT_RATE && service.getPrice() != null) {
|
||||
total = total.add(service.getPrice());
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.DISTANCE
|
||||
&& service.getPricePerKilometer() != null && kilometersField != null
|
||||
&& kilometersField.getValue() != null) {
|
||||
BigDecimal kilometers = new BigDecimal(kilometersField.getValue());
|
||||
&& service.getPricePerKilometer() != null && currentJob.getRouteDistanceKm() != null) {
|
||||
BigDecimal kilometers = BigDecimal.valueOf(currentJob.getRouteDistanceKm());
|
||||
BigDecimal serviceTotal = service.getPricePerKilometer().multiply(kilometers);
|
||||
total = total.add(serviceTotal);
|
||||
} else if (service.getCalculationBasis() == Service.CalculationBasis.TIME
|
||||
&& service.getPricePer15Minutes() != null && timeField != null && timeField.getValue() != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(timeField.getValue());
|
||||
&& service.getPricePer15Minutes() != null && currentJob.getTimeIn15MinUnits() != null) {
|
||||
BigDecimal timeUnits = new BigDecimal(currentJob.getTimeIn15MinUnits());
|
||||
BigDecimal serviceTotal = service.getPricePer15Minutes().multiply(timeUnits);
|
||||
total = total.add(serviceTotal);
|
||||
}
|
||||
@@ -402,14 +366,6 @@ public class CreateInvoiceView extends VerticalLayout implements HasUrlParameter
|
||||
}
|
||||
|
||||
private void updateSummarySection() {
|
||||
// Update the job with new values
|
||||
if (kilometersField != null && kilometersField.getValue() != null) {
|
||||
currentJob.setKilometersDriven(kilometersField.getValue());
|
||||
}
|
||||
if (timeField != null && timeField.getValue() != null) {
|
||||
currentJob.setTimeIn15MinUnits(timeField.getValue());
|
||||
}
|
||||
|
||||
// Refresh the services grid to update calculated prices
|
||||
refreshServicesGrid();
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
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.IFrame;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import com.vaadin.flow.server.StreamResource;
|
||||
import de.assecutor.votianlt.pages.base.ui.view.AdminLayout;
|
||||
import de.assecutor.votianlt.service.CustomerInvoiceService;
|
||||
import de.assecutor.votianlt.service.SystemInvoiceService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
@Route(value = "pdf-test", layout = AdminLayout.class)
|
||||
@PageTitle("PDF Test")
|
||||
@RolesAllowed("ADMIN")
|
||||
public class PdfTestView extends VerticalLayout {
|
||||
private final SystemInvoiceService systemInvoiceService;
|
||||
private final CustomerInvoiceService customerInvoiceService;
|
||||
|
||||
public PdfTestView(SystemInvoiceService systemInvoiceService, CustomerInvoiceService customerInvoiceService) {
|
||||
this.systemInvoiceService = systemInvoiceService;
|
||||
this.customerInvoiceService = customerInvoiceService;
|
||||
|
||||
setSpacing(false);
|
||||
setPadding(false);
|
||||
getStyle().set("margin", "14px");
|
||||
setWidth("90%");
|
||||
|
||||
H2 title = new H2("PDF Test");
|
||||
add(title);
|
||||
|
||||
Button generateHtmlPdfButton = new Button("PDF aus system_invoice.html generieren");
|
||||
generateHtmlPdfButton.addClickListener(e -> generateHtmlPdf());
|
||||
|
||||
Button generateCustomerInvoicePdfButton = new Button("PDF aus customer_invoice.html generieren");
|
||||
generateCustomerInvoicePdfButton.addClickListener(e -> generateCustomerInvoicePdf());
|
||||
|
||||
// Create button layout
|
||||
HorizontalLayout buttonLayout = new HorizontalLayout();
|
||||
buttonLayout.add(generateHtmlPdfButton, generateCustomerInvoicePdfButton);
|
||||
buttonLayout.setSpacing(true);
|
||||
|
||||
// Initialize PDF viewer
|
||||
IFrame pdfViewer = new IFrame();
|
||||
pdfViewer.setWidth("100%");
|
||||
pdfViewer.setHeight("800px");
|
||||
pdfViewer.getStyle().set("border", "1px solid #ccc");
|
||||
pdfViewer.setVisible(false);
|
||||
|
||||
add(buttonLayout);
|
||||
add(pdfViewer);
|
||||
}
|
||||
|
||||
private void generateHtmlPdf() {
|
||||
try {
|
||||
byte[] pdfBytes = systemInvoiceService.generateInvoicePdfFromHtml();
|
||||
|
||||
StreamResource resource = new StreamResource("vlt-invoice.pdf", () -> new ByteArrayInputStream(pdfBytes));
|
||||
resource.setContentType("application/pdf");
|
||||
|
||||
getUI().ifPresent(ui -> {
|
||||
var registration = ui.getSession().getResourceRegistry().registerResource(resource);
|
||||
ui.getPage().open(registration.getResourceUri().toString(), "_blank");
|
||||
});
|
||||
|
||||
Notification.show("PDF aus HTML erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Fehler beim Generieren des PDFs aus HTML: " + ex.getMessage(), 5000,
|
||||
Notification.Position.BOTTOM_CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateCustomerInvoicePdf() {
|
||||
try {
|
||||
byte[] pdfBytes = customerInvoiceService.generateCustomerInvoicePdf();
|
||||
|
||||
StreamResource resource = new StreamResource("customer-invoice.pdf",
|
||||
() -> new ByteArrayInputStream(pdfBytes));
|
||||
resource.setContentType("application/pdf");
|
||||
|
||||
getUI().ifPresent(ui -> {
|
||||
var registration = ui.getSession().getResourceRegistry().registerResource(resource);
|
||||
ui.getPage().open(registration.getResourceUri().toString(), "_blank");
|
||||
});
|
||||
|
||||
Notification.show("Customer PDF erfolgreich generiert!", 3000, Notification.Position.BOTTOM_CENTER);
|
||||
} catch (Exception ex) {
|
||||
Notification.show("Fehler beim Generieren des Customer PDFs: " + ex.getMessage(), 5000,
|
||||
Notification.Position.BOTTOM_CENTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user