Document legacy workflows and stabilize migrated services
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(bash:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,10 +106,12 @@ Die PHP-Anwendung wird schrittweise in `services` und `vaadin` ueberfuehrt. Dies
|
|||||||
- Der Kommunikationsblock ist jetzt als eigener HQ-Workspace portiert: NG konsolidiert `admin/newsticker.php`, `admin/courier_msggrp.php`, `admin/mf_history.php` und die zugehoerigen Legacy-Nebenpfade in einem Bereich fuer Mitteilungen auf `phoenix_group.tickerforum`, Nachrichtengruppen und Kurierzuordnungen auf `messagegroup`/`courier.cr_msggrp` sowie Endgeraetehistorie und Versand-/Antwortlogik auf `phoenix_log.messageforum`
|
- Der Kommunikationsblock ist jetzt als eigener HQ-Workspace portiert: NG konsolidiert `admin/newsticker.php`, `admin/courier_msggrp.php`, `admin/mf_history.php` und die zugehoerigen Legacy-Nebenpfade in einem Bereich fuer Mitteilungen auf `phoenix_group.tickerforum`, Nachrichtengruppen und Kurierzuordnungen auf `messagegroup`/`courier.cr_msggrp` sowie Endgeraetehistorie und Versand-/Antwortlogik auf `phoenix_log.messageforum`
|
||||||
- Die verbliebenen Newsletter-/Legacy-Nebenpfade sind jetzt fachlich abgeschlossen: NG pflegt Newsletter-Opt-in und DSGVO-Status fuer Kunden- und Kurierfirmen direkt im Kommunikationsworkspace auf `company.cmp_newsletter`/`company.cmp_dsgvo`, waehrend nicht mehr belastbar referenzierte Sonderpfade wie `sysadmin/newsletter/*.php`, `tools/auto_response*.php` und generische `admin/metafield_special_cron.php`-Einmalpfade bewusst als Legacy-Archiv dokumentiert statt weiter stillschweigend als offene NG-Workspaces mitzulaufen; die Bewertungsbasis steht in `LEGACY_SIDEPATH_AUDIT.md`
|
- Die verbliebenen Newsletter-/Legacy-Nebenpfade sind jetzt fachlich abgeschlossen: NG pflegt Newsletter-Opt-in und DSGVO-Status fuer Kunden- und Kurierfirmen direkt im Kommunikationsworkspace auf `company.cmp_newsletter`/`company.cmp_dsgvo`, waehrend nicht mehr belastbar referenzierte Sonderpfade wie `sysadmin/newsletter/*.php`, `tools/auto_response*.php` und generische `admin/metafield_special_cron.php`-Einmalpfade bewusst als Legacy-Archiv dokumentiert statt weiter stillschweigend als offene NG-Workspaces mitzulaufen; die Bewertungsbasis steht in `LEGACY_SIDEPATH_AUDIT.md`
|
||||||
- Der HQ-Operationsblock fuer Karten, Adressen und Suche ist jetzt als eigener NG-Workspace portiert: NG konsolidiert `locating/map.php`, `admin/ad_admin.php`, `admin/nearBySearch.php` und `admin/traveltime.php` in einer Route fuer globale Kartenuebersicht mit HQ-/Kurierpunkten, direkte Pflege von `address` und `phoenix_special.street`, fuzzy Aehnlichkeitssuche fuer Kunden und Kuriere sowie Legacy-kompatible Pflege der `serviceplz`-/`serviceplztraveltime`-Anfahrtszeiten; Rechte und Sichtbarkeit folgen dabei wieder den Legacy-Bits `19`, `2`, `0` und `1`
|
- Der HQ-Operationsblock fuer Karten, Adressen und Suche ist jetzt als eigener NG-Workspace portiert: NG konsolidiert `locating/map.php`, `admin/ad_admin.php`, `admin/nearBySearch.php` und `admin/traveltime.php` in einer Route fuer globale Kartenuebersicht mit HQ-/Kurierpunkten, direkte Pflege von `address` und `phoenix_special.street`, fuzzy Aehnlichkeitssuche fuer Kunden und Kuriere sowie Legacy-kompatible Pflege der `serviceplz`-/`serviceplztraveltime`-Anfahrtszeiten; Rechte und Sichtbarkeit folgen dabei wieder den Legacy-Bits `19`, `2`, `0` und `1`
|
||||||
|
- Portierte Alt-Crons mit echtem NG-Fachnachfolger laufen jetzt direkt als Spring-Scheduler im Backend: Auth-Session-Cleanup, der Auto-Logout fuer per Autorevoke haengende PDA-Kuriere sowie Mail-/FTP-Versand fuer Abnahmeprotokolle nutzen native Java-Services, Legacy-Parameter und GDC-Statusfelder statt separater Shell-/PHP-Cronstarter
|
||||||
|
- Rein technische Serverjobs wie `bzip2`, `rsync` und Replikationsmonitoring sowie bewusst archivierte Spezialimporte/Partnerexporte bleiben weiterhin ausserhalb des Spring-Boot-Schedulers, weil sie entweder Infrastruktur statt Fachlogik sind oder laut Audit keinen nativen NG-Nachfolger mehr haben
|
||||||
|
|
||||||
## Noch offen
|
## Noch offen
|
||||||
|
|
||||||
- Keine weiteren grossen fachlichen Legacy-Arbeitsbereiche mehr offen; verbleibend sind nur technische Betriebsentscheidungen fuer Alt-Crons, Einmalintegrationen und Archivpfade ausserhalb der produktiven NG-UI.
|
- Keine weiteren grossen fachlichen Legacy-Arbeitsbereiche mehr offen; verbleibend sind nur noch Infrastruktur-/Archiventscheidungen fuer Serverjobs, Alt-Partnerexporte und Einmalintegrationen ausserhalb der produktiven NG-UI.
|
||||||
|
|
||||||
## Nächste empfohlene Vertikalschnitte
|
## Nächste empfohlene Vertikalschnitte
|
||||||
|
|
||||||
|
|||||||
1215
html/DOKUMENTATION.md
Normal file
1215
html/DOKUMENTATION.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,445 +0,0 @@
|
|||||||
# Dokumentation: Niederlassungen der Stadtbote GmbH
|
|
||||||
|
|
||||||
## Phoenix-Portal -- Konfiguration und Abweichungen pro Niederlassung
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Systemarchitektur
|
|
||||||
|
|
||||||
Das Phoenix-Portal ist eine **Multi-Tenant PHP-Anwendung** zur Auftragsannahme und -vermittlung in der Logistikbranche. Jede Niederlassung (Headquarter/HQ) wird durch eine eindeutige `hq_id` identifiziert und teilt sich eine gemeinsame Codebasis mit individuellen Konfigurationsparametern.
|
|
||||||
|
|
||||||
### Konfigurationsebenen (Hierarchie)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Global (hq_id=0, emp_id=0) -- Systemweite Standardwerte
|
|
||||||
2. Niederlassung (hq_id=N, emp_id=0) -- HQ-spezifische Overrides
|
|
||||||
3. Mitarbeiter (hq_id=N, emp_id=M) -- Individuelle Einstellungen
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Funktion `getParameterValue($empId, $key, $hqId)` in `include/dbglobal.inc.php` liest Parameter in dieser Reihenfolge: Mitarbeiter -> Niederlassung -> Global.
|
|
||||||
|
|
||||||
### Kernkomponenten
|
|
||||||
|
|
||||||
| Datei | Funktion |
|
|
||||||
|---|---|
|
|
||||||
| `include/dbglobal.inc.php` | Parameter-Verwaltung (`getParameterValue`, `setParameterValue`, `defineGlobalParameters`) |
|
|
||||||
| `include/auth.inc.php` | Authentifizierung und HQ-Zugriffskontrolle |
|
|
||||||
| `admin/hq_admin.php` | Admin-Oberflaeche "NIEDERLASSUNGEN" |
|
|
||||||
| `include/services_func.inc.php` | Preisberechnung pro HQ |
|
|
||||||
| `include/mcglobal.inc.php` | Datenbankfeld-Definitionen mit HQ-Bezug |
|
|
||||||
|
|
||||||
### Datenbanktabellen mit HQ-Bezug
|
|
||||||
|
|
||||||
| Tabelle | HQ-Felder | Zweck |
|
|
||||||
|---|---|---|
|
|
||||||
| `headquarters` | `hq_id`, `hq_mnemonic`, `hq_name` | Niederlassungsdefinitionen |
|
|
||||||
| `mandatorheadquarters` | `md_id`, `hq_id` | Zuordnung Mandant zu Niederlassung |
|
|
||||||
| `parameter` | `par_key`, `hq_id`, `emp_id`, `par_value` | Konfigurationsparameter |
|
|
||||||
| `job` | `hq_id_exec`, `hq_id_dispo`, `hq_id_sales` | Auftragszuordnung (Ausfuehrung, Disposition, Vertrieb) |
|
|
||||||
| `customer` | `hq_id` | Kundenzuordnung |
|
|
||||||
| `courier` | `hq_id` | Kurierzuordnung |
|
|
||||||
| `tour` | `hq_id_dispo` | Tourenzuordnung |
|
|
||||||
| `servicehistory` | `hq_id` | Preistabellen pro HQ |
|
|
||||||
| `serviceplzhistory` | `hq_id` | PLZ-basierte Preise pro HQ |
|
|
||||||
| `serviceplzareahistory` | `hq_id` | PLZ-Gebiet-Preise pro HQ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Uebersicht der Niederlassungen
|
|
||||||
|
|
||||||
| hq_id | Kuerzel | HQ_INSTANCE | Stadt | MANDATOR_PREFIX | EXPORT_HQ_KEY |
|
|
||||||
|-------|---------|-------------|-------|-----------------|---------------|
|
|
||||||
| 0 | -- | (global) | -- | -- | -- |
|
|
||||||
| 101 | HT_HB | HT_HB | Bremen | HTHB | 003 |
|
|
||||||
| 102 | HT_HH | HT_HH | Hamburg | HTHH | 001 |
|
|
||||||
| 103 | HT_B | HT_B | Berlin | HTB | 007 |
|
|
||||||
| 104 | HT_H | HT_H | Hannover | HTH | 006 |
|
|
||||||
| 105 | HT_F | HT_F | Frankfurt | HTF | 002 |
|
|
||||||
| 106 | HT_DD | HT_DD | Dresden | HTDD | 005 |
|
|
||||||
| 107 | HT_E | HT_E | Essen | HTE | 009 |
|
|
||||||
| 108 | HT_L | HT_L | Leipzig | HTL | 008 |
|
|
||||||
| 109 | HT_M | HT_M | Muenchen | HTM | 013 |
|
|
||||||
| 110 | HT_N | HT_N | Nuernberg | HTN | 023 |
|
|
||||||
| 111 | HT_S | HT_S | Stuttgart | HTS | 014 |
|
|
||||||
| 112 | HT_K | HT_K | Koeln | HTK | 015 |
|
|
||||||
| 203 | HT_LG | HT_LG | Logistics (Zentrale) | HTLG | 010 |
|
|
||||||
|
|
||||||
Alle Niederlassungen teilen: `SRV_INSTANCE = HT1`, `PATH_DOCROOT = /home/www/hansetrans`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Globale Einstellungen (fuer alle NL gleich)
|
|
||||||
|
|
||||||
### 3.1 System & Sicherheit
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `TA_STATUS` | 1 | Transaktionsmodus aktiviert (InnoDB) |
|
|
||||||
| `AD_STATUS` | 1 | Adressprufung aktiv |
|
|
||||||
| `MG_STATUS` | 0 | M&G-Server Adressprufung deaktiviert |
|
|
||||||
| `LOG_DB` | 1 | Datenbank-Logging aktiv |
|
|
||||||
| `HTTP_VARS_SEC_STATE` | 1 | HTTP-Parameter-Verschluesselung aktiv |
|
|
||||||
| `HTTP_VARS_SEC_SEQ` | __ | Verschluesselungs-Identifikator |
|
|
||||||
| `ENCRYPT_EXPORTDATA` | 0 | Export-Datenverschluesselung deaktiviert |
|
|
||||||
| `ENCRYPT_FILEEXTENSION` | gpg | Verschluesselungs-Dateiendung |
|
|
||||||
|
|
||||||
### 3.2 Automailer
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `AUTOMAILER_ENABLED` | 1 | Automatischer Mailversand aktiv |
|
|
||||||
| `AUTOMAILER_STARTTIME_IN_DAYS` | 3 | Mails fuer Auftraege der letzten 3 Tage |
|
|
||||||
| `AUTOMAILER_LOGFILE` | ../log/automailer.log | Logdatei-Pfad |
|
|
||||||
| `AUTOMAILER_SLEEP_TIME` | 5 | Wartezeit zwischen Mails (Sekunden) |
|
|
||||||
|
|
||||||
### 3.3 Autoranking (Kurierzuweisung)
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `AUTORANKING_ASSIGNMENT_ENABLED` | 0 | Automatische Zuweisung deaktiviert |
|
|
||||||
| `AUTORANKING_REVOCATION_ENABLED` | 1 | Ruecknahme aktiviert |
|
|
||||||
| `AUTORANKING_REVOKETIME_IN_MINUTES` | 1 | Ruecknahme nach 1 Min. (automat.) |
|
|
||||||
| `AUTORANKING_REVOKETIME_MANUELL_IN_MINUTES` | 3 | Ruecknahme nach 3 Min. (manuell) |
|
|
||||||
| `AUTORANKING_MAXNUMBER_OF_CHALLENGES` | 2 | Max. Anfragen pro Kurier |
|
|
||||||
| `AUTORANKING_NUMBER_OF_ITERATIONS` | 2 | Anzahl Iterationen |
|
|
||||||
| `AUTORANKING_NEIGHBOUR_LEVEL` | 1 | Nachbargebiete pruefen: 1 Ebene |
|
|
||||||
| `AUTORANKING_VEHICLE_LKW` | 10 | Fahrzeugtyp ab dem LKW-Jobs vergeben werden |
|
|
||||||
| `RANKING_CR2CRVH_MULTI_RELATION` | 1 | Kurier-Fahrzeug-Pflichtverknuepfung |
|
|
||||||
| `RANKING_FAVOURED_COURIER_FOR_PAYER` | 1 | Bevorzugte Kuriere fuer Zahler pruefen |
|
|
||||||
| `RANKING_FAVOURED_COURIER_FOR_STATION` | 0 | Bevorzugte Kuriere fuer Stationen nicht pruefen |
|
|
||||||
| `RANKING_FAVOURED_COURIER_AREA_RESTRICTION` | 0 | Keine Gebietseinschraenkung fuer bevorzugte Kuriere |
|
|
||||||
|
|
||||||
### 3.4 Geo & Lokalisierung
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `GEO_EARTH_RADIUS` | 6371.0 | Erdradius in km (WGS84 Mittelwert) |
|
|
||||||
| `LOCATING_MODE` | 0 | Polygon-Modus (Standard) |
|
|
||||||
| `MAXIMUM_SEARCH_RADIUS` | 20 | Suchradius in km |
|
|
||||||
| `LOCATING_LBS_SERVER` | 139.7.25.166 | LBS-Server IP |
|
|
||||||
|
|
||||||
### 3.5 Auftragserfassung (UI)
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `MASK_CALCULATOR` | 1 | Preisrechner aktiviert |
|
|
||||||
| `MASK_CALCULATOR_SRV` | 1 | Services im Rechner waehlbar |
|
|
||||||
| `MASK_MANUAL_DISPOSITION` | 1 | Manuelle Disposition als Standard |
|
|
||||||
| `MASK_COMMISSION_NO` | 1 | Kommissionsnummer immer aktiviert |
|
|
||||||
| `MASK_CASH_PAYER_SELECT` | 1 | Barzahler muss gewaehlt werden |
|
|
||||||
| `MASK_ASK_DEFAULTPAYER_CHANGE` | 1 | Nachfrage bei Zahleraenderung |
|
|
||||||
| `MASK_COURIERDETAILS_TARGET` | 1 | Kurierdetails in separatem Fenster |
|
|
||||||
| `MASK_CUSTOMERDETAILS_TARGET` | 1 | Kundendetails in separatem Fenster |
|
|
||||||
| `MASK_LOCKTIME_TIMEOUT` | 5 | Sperr-Timeout: 5 Minuten |
|
|
||||||
| `MASK_JOBLIST_BROWSE_MAX` | 100 | Max. 100 Zeilen in Auftragsliste |
|
|
||||||
| `MASK_JOBLIST_DEFAULTLIST` | 8,9,0,1 | Standard-Auftragslisten: Vermittlung, Abgeholt, Offen, Zugewiesen |
|
|
||||||
| `MASK_DATE_PLUSOFFSETDAYS` | 2 | Datumsoffset: 2 Tage voraus |
|
|
||||||
| `MASK_COURIER_FREETIME_MINUTES` | 30 | Kurier als frei: 30 Min. vor Auftragszeit |
|
|
||||||
| `MASK_COURIER_NEWBIE_TIME` | 30 | Kurier als Neuling markiert: 30 Tage |
|
|
||||||
| `LATEST_TAKETIME_IN_MINUTES` | 30 | Standard-Uebernahmezeit: 30 Min. |
|
|
||||||
|
|
||||||
### 3.6 Rechnungswesen
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `INV_MAXCOLS` | 50 | Max. Spalten Rechnungstext (Anzeige) |
|
|
||||||
| `INV_MAXCOLS_EXPORT` | 70 | Max. Spalten Rechnungstext (Export) |
|
|
||||||
| `INV_JB_CR_PRICE` | 1 | Fuhrlohn in Rechnungsmodul anzeigen |
|
|
||||||
| `INV_JB_INVOICE_CR` | 1 | Separate Kurierrechnung generieren |
|
|
||||||
| `INV_PRINT_DISCOUNT` | 1 | Rabatt auf Rechnung drucken |
|
|
||||||
| `INV_PRINT_REMARK` | 0 | Bemerkung nicht auf Rechnung drucken |
|
|
||||||
| `JB_PAYMODE_CASH` | BZ | Barzahlung-Kuerzel |
|
|
||||||
| `JB_TAX_RATE_SIGN` | OM | Standard-Steuersatz-Kennzeichen |
|
|
||||||
|
|
||||||
### 3.7 FTP & Export-Pfade
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `FTP_SERVER` | 172.16.0.104 | Interner FTP-Server |
|
|
||||||
| `FTP_USER` | sap | FTP-Benutzer |
|
|
||||||
| `FTP_UPLOADPATH` | /stadtbote/ | Upload-Verzeichnis |
|
|
||||||
| `EXPORT_PATH` | ../export/download/ | Export-Dateipfad |
|
|
||||||
| `EXPORT_FILES_ON_SERVER` | 100 | Max. Export-Dateien (HQ) |
|
|
||||||
| `EXPORT_FILES_ON_SERVER_CUSTOMER` | 10 | Max. Export-Dateien (Kunde) |
|
|
||||||
|
|
||||||
### 3.8 Sonstige globale Einstellungen
|
|
||||||
|
|
||||||
| Parameter | Wert | Beschreibung |
|
|
||||||
|---|---|---|
|
|
||||||
| `COUNTRY_FON_PREFIX` | 49 | Laendervorwahl Deutschland |
|
|
||||||
| `ZIPCODE_LENGTH` | 5 | PLZ-Laenge |
|
|
||||||
| `ZIPCODEAREA_PADLENGTH` | 4 | PLZ-Gebiet-Laenge (z.B. "0057") |
|
|
||||||
| `MODE_INTERMEDIATION` | 2 | Vermittlungsmodus: PLZ-Gebiet |
|
|
||||||
| `MESSAGE_MAX_BODY_LENGTH` | 200 | Max. Nachrichtenlaenge |
|
|
||||||
| `MASK_MARKUP_MODE` | 2 | Kraftstoffzuschlag-Modus: HT |
|
|
||||||
| `MANDATOR_SERVICE_ENABLED` | 1 | Service-Modul aktiviert |
|
|
||||||
| `MANDATOR_SERVICE2_ENABLED` | 1 | Service2-Modul aktiviert |
|
|
||||||
| `MD_GLOBAL_SHORTNAME` | HTG | Globales Mandantenkuerzel |
|
|
||||||
| `MASTER_PREFIX` | HT | Master-Praefix |
|
|
||||||
| `FRAMEWORK_USED` | 1 | Framework aktiviert |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Abweichungen pro Niederlassung
|
|
||||||
|
|
||||||
### 4.1 Kostenstellen (CSC_ID_PAYER)
|
|
||||||
|
|
||||||
| NL | Stadt | CSC_ID_PAYER_CASH | CSC_ID_PAYER_EXTERN | CSC_ID_PAYER_CALCULATOR |
|
|
||||||
|----|-------|-------------------|---------------------|------------------------|
|
|
||||||
| 101 | Bremen | 5688 | 44539 | individuell |
|
|
||||||
| 102 | Hamburg | 7797 | 44540 | individuell |
|
|
||||||
| 103 | Berlin | 6758 | 44541 | individuell |
|
|
||||||
| 104 | Hannover | 7402 | 44542 | individuell |
|
|
||||||
| 105 | Frankfurt | 9286 | 44543 | individuell |
|
|
||||||
| 106 | Dresden | 6533 | 44544 | individuell |
|
|
||||||
| 107 | Essen | 9374 | 44545 | individuell |
|
|
||||||
| 108 | Leipzig | 9117 | 44546 | individuell |
|
|
||||||
| 109 | Muenchen | 9750 | 44547 | individuell |
|
|
||||||
| 110 | Nuernberg | 10041 | 44548 | individuell |
|
|
||||||
| 111 | Stuttgart | 10142 | 44549 | individuell |
|
|
||||||
| 112 | Koeln | 55240 | 55241 | individuell |
|
|
||||||
| 203 | Logistics | 6758* | 44541* | individuell |
|
|
||||||
|
|
||||||
*) LG teilt Kostenstellen mit Berlin
|
|
||||||
|
|
||||||
### 4.2 EID-Nummernkreise (Kunden/Kuriere/Artikel)
|
|
||||||
|
|
||||||
| NL | Stadt | CS_EID_GENERATION | CR_EID_GENERATION | CS_EID_PREFIX | CR_EID_PREFIX | AT_EID_PREFIX |
|
|
||||||
|----|-------|-------------------|-------------------|---------------|---------------|---------------|
|
|
||||||
| 101 | Bremen | HTHB64999 | 28800 | HB | HB | HB |
|
|
||||||
| 102 | Hamburg | HTHH64999 | 28800 | HH | HH | HH |
|
|
||||||
| 103 | Berlin | HTB64999 | 28800 | B | B | B |
|
|
||||||
| 104 | Hannover | HTH64999 | 28800 | H | H | H |
|
|
||||||
| 105 | Frankfurt | HTF64999 | 28800 | F | F | F |
|
|
||||||
| 106 | Dresden | HTDD64999 | 28800 | DD | DD | DD |
|
|
||||||
| 107 | Essen | HTE64999 | 28800 | E | E | E |
|
|
||||||
| 108 | Leipzig | HTL64999 | 28800 | L | L | L |
|
|
||||||
| 109 | Muenchen | HTM64999 | 28800 | M | M | M |
|
|
||||||
| 110 | Nuernberg | HTN64999 | 28800 | N | N | N |
|
|
||||||
| 111 | Stuttgart | HTS64999 | 28800 | S (aber ESL*) | S (aber ES*) | S (aber ES*) |
|
|
||||||
| **112** | **Koeln** | **HTK79999** | **HTK89999** | HTK | HTK | HTK |
|
|
||||||
| 203 | Logistics | HTLG64999 | 28800 | LG | LG | LG |
|
|
||||||
|
|
||||||
**Abweichung Koeln (112):** Abweichende Nummernkreise (79999 statt 64999, HTK89999 statt 28800). Koeln verwendet einen hoeheren Bereich und alphanumerische Praefixe in der Kurier-EID-Generierung.
|
|
||||||
|
|
||||||
### 4.3 E-Mail-Konfiguration
|
|
||||||
|
|
||||||
| NL | MAIL_SENDER_ADDRESS | MAIL_BCC_ADDRESS | MAIL_SALUTATION_TEXT |
|
|
||||||
|----|---------------------|------------------|----------------------|
|
|
||||||
| 101 | hb.transport@hansetrans.de | hb.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 102 | hh.transport@hansetrans.de | hh.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 103 | b.transport@hansetrans.de | b.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 104 | h.transport@hansetrans.de | h.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 105 | f.transport@hansetrans.de | f.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 106 | dd.transport@hansetrans.de | dd.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 107 | e.transport@hansetrans.de | e.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| **108** | **HANSETRANS Leipzig <l.transport@hansetrans.de>** | l.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 109 | m.transport@hansetrans.de | m.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 110 | n.transport@hansetrans.de | n.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 111 | info@es-l.eu* | (leer) | Ihre ES-Logistic* |
|
|
||||||
| 112 | k.transport@hansetrans.de | k.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
| 203 | fnl.transport@hansetrans.de | fnl.transport@hansetrans.de | Ihre HANSETRANS |
|
|
||||||
|
|
||||||
**Abweichung Leipzig (108):** Verwendet Absendername mit Klammern-Format `HANSETRANS Leipzig <...>` statt nur E-Mail-Adresse.
|
|
||||||
|
|
||||||
**Abweichung Stuttgart (111):** Laeuft unter der Marke **ES-Logistic** (eigene Domain `es-l.eu`) statt Hansetrans.
|
|
||||||
|
|
||||||
### 4.4 PDA-Lokalisierung
|
|
||||||
|
|
||||||
| NL | LOCATING_PDA_ENABLED | LOCATING_PDA_INTERVAL |
|
|
||||||
|----|---------------------|-----------------------|
|
|
||||||
| 101 Bremen | 1 (aktiv) | **8,0,9,0** (nur 08:00-09:00) |
|
|
||||||
| 102 Hamburg | 1 (aktiv) | **0,0,12,13** (00:00-12:13) |
|
|
||||||
| 103-107, 109-112 | 1 (aktiv) | 0,0,23,59 (ganztaegig) |
|
|
||||||
| **108 Leipzig** | **0 (deaktiviert)** | 0,0,23,59 |
|
|
||||||
| 203 Logistics | 0 (deaktiviert) | 0,0,23,59 |
|
|
||||||
|
|
||||||
**Abweichungen:**
|
|
||||||
- **Bremen:** Extrem eingeschraenktes Lokalisierungsfenster (nur 1 Stunde morgens)
|
|
||||||
- **Hamburg:** Halbtaegiges Fenster bis mittags
|
|
||||||
- **Leipzig & Logistics:** PDA-Lokalisierung komplett deaktiviert
|
|
||||||
|
|
||||||
### 4.5 Max. Stationen pro Tour (MASK_MAXTOUR)
|
|
||||||
|
|
||||||
| NL | Wert |
|
|
||||||
|----|------|
|
|
||||||
| **102 Hamburg** | **49** |
|
|
||||||
| **105 Frankfurt** | **49** |
|
|
||||||
| **109 Muenchen** | **49** |
|
|
||||||
| Alle anderen | 19 |
|
|
||||||
|
|
||||||
Hamburg, Frankfurt und Muenchen erlauben wesentlich mehr Stationen pro Auftrag.
|
|
||||||
|
|
||||||
### 4.6 Kundenprovision
|
|
||||||
|
|
||||||
| Parameter | Bremen (101) | Hannover (104) | Alle anderen |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `JB_EDITBATCH_CS_PROV_ENABLED` | **1** (aktiv) | **1** (aktiv) | 0 (deaktiviert) |
|
|
||||||
| `MASK_CS_PROV_DEFAULT` | **14.00%** | **10.0%** | 0% |
|
|
||||||
|
|
||||||
Nur Bremen und Hannover haben die Kundenprovisionsberechnung aktiviert.
|
|
||||||
|
|
||||||
### 4.7 Rechnungseinstellungen (Abweichungen)
|
|
||||||
|
|
||||||
| Parameter | Beschreibung | Abweichende NL | Wert | Standard |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `INV_JB_CR_PRICE_STATIONS` | Stationen im Kurier-Rechnungsmodul | **101 Bremen, 103 Berlin** | **1** (aktiv) | 0 |
|
|
||||||
| `INV_SHOW_INVOICE_TEXT` | Rechnungstext fuer Kuriere sichtbar | **101 Bremen, 106 Dresden, 108 Leipzig** | **0** (deaktiviert) | 1 |
|
|
||||||
|
|
||||||
### 4.8 Auftragsverhalten (Abweichungen)
|
|
||||||
|
|
||||||
| Parameter | Beschreibung | Abweichende NL | Wert | Standard |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `MODE_COPY_JOB_DISPOINFO` | Dispoinfo beim Kopieren ignorieren | **101 Bremen** | **1** | 0 |
|
|
||||||
| `MASK_JB_TYPE_DEFAULT` | Standard-Auftragstyp | alle NL | 2 (Guetertaxi) | -- |
|
|
||||||
| `MASK_INVOICE_SIDS_SID` | Spezial-SID fuer Multi-SID | **111 Stuttgart** | **S888** | {Stadt}1888 |
|
|
||||||
|
|
||||||
### 4.9 Mail-Footer (Adresse & Kontakt)
|
|
||||||
|
|
||||||
Jede Niederlassung hat individuelle Postadresse und Telefonnummern im Mail-Footer. Beispiele:
|
|
||||||
|
|
||||||
| NL | Adresse | Telefon |
|
|
||||||
|----|---------|---------|
|
|
||||||
| 101 Bremen | Hansetrans Guetertaxi GmbH, Am Wall 175, 28195 Bremen | 0421 39 39 39 |
|
|
||||||
| 102 Hamburg | Hansetrans Guetertaxi GmbH, Nagelsweg 10, 20097 Hamburg | 040 41 41 41 |
|
|
||||||
| 103 Berlin | Hansetrans Guetertaxi GmbH, Motzener Str. 6, 12277 Berlin | 030 75 75 75 |
|
|
||||||
| 111 Stuttgart | ES-Logistic GmbH, Ulmer Strasse 53/1, 73262 Reichenbach | 07153 99 67 353 |
|
|
||||||
|
|
||||||
### 4.10 Cron-Benachrichtigungen
|
|
||||||
|
|
||||||
Alle Niederlassungen senden Cron-Mails an `mail-cron@assecutor.de`, ergaenzt um standortspezifische Adressen:
|
|
||||||
- `kennziffer95-{stadt}@hansetrans.de`
|
|
||||||
- `eg-lizenz-{stadt}@hansetrans.de`
|
|
||||||
- `vorortpruefung-{stadt}@hansetrans.de`
|
|
||||||
- `unternehmerbefragung-{stadt}@hansetrans.de`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Niederlassungs-spezifische Dateien (Code-Ebene)
|
|
||||||
|
|
||||||
### 5.1 Cron-Jobs
|
|
||||||
|
|
||||||
| Datei | Niederlassung(en) | Funktion |
|
|
||||||
|---|---|---|
|
|
||||||
| `tools/cron_export_HTG.php` | HTG (alle NL) | RETRANS-Export mit Mapping aller 12 NL-IDs |
|
|
||||||
| `sysadmin/cron/cron_export_HTG.php` | HTG | Haupt-Export-Cron |
|
|
||||||
| `sysadmin/cron/cron_export_HTM.php` | HTM (109 Muenchen) | Moebel/Kuechen-Export |
|
|
||||||
| `export/cron_export_HHA.php` | HHA | Hamburg-spezifischer Export |
|
|
||||||
| `tools/cron_export_SB_MC.php` | MC | Maritim-spezifischer Export |
|
|
||||||
| `tools/cron_FTP_upload_HTG.php` | HTG | FTP-Upload fuer alle NL |
|
|
||||||
|
|
||||||
### 5.2 Spezialmodule
|
|
||||||
|
|
||||||
| Datei | Niederlassung | Funktion |
|
|
||||||
|---|---|---|
|
|
||||||
| `admin/jb_list_MC.php` | MC | Spezielle Auftragsliste |
|
|
||||||
| `statistic/statistic_MC.php` | MC | Eigenes Statistikmodul |
|
|
||||||
| `statistic/statistic_interface_cs_MC.inc.php` | MC | Kundenstatistik-Interface |
|
|
||||||
| `statistic/statistic_interface_hq_MC.inc.php` | MC | NL-Statistik-Interface |
|
|
||||||
| `include/jb_list_defineoutput_MC.inc.php` | MC | Spezielle Listenausgabe |
|
|
||||||
| `tools/auto_response_MC.php` | MC | Spezielle Auto-Antwort |
|
|
||||||
| `tools/statistic_special_GFL_MC.php` | MC | GFL-Spezialstatistik |
|
|
||||||
| `tools/statistic_HTM_01.php` | HTM | Kuechen-Montage-Statistik |
|
|
||||||
|
|
||||||
### 5.3 Dynamisch geladene Module
|
|
||||||
|
|
||||||
| Muster | Lademechanismus | Beispiele |
|
|
||||||
|---|---|---|
|
|
||||||
| `import/import_*.php` | `data_transfer.php` via `$f_ftp_servername` | import_HHA.php, import_FAMO.php, import_BMW.php, etc. |
|
|
||||||
| `tools/auto_response_*.php` | `auto_response.php` via Kundenkennung | auto_response_MC.php, etc. |
|
|
||||||
| `tools/auto_export_*.php` | `auto_export.php` | auto_export_stkat_DKT.php, etc. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. HTG Sub-Niederlassungen (Cron-Export-Mapping)
|
|
||||||
|
|
||||||
Die Datei `tools/cron_export_HTG.php` definiert das Mapping aller HTG-Niederlassungen:
|
|
||||||
|
|
||||||
| hq_id | Kuerzel | Stadt | FTP-Endpunkt |
|
|
||||||
|-------|---------|-------|-------------|
|
|
||||||
| 101 | HB | Bremen | eigener FTP-Pfad |
|
|
||||||
| 102 | HH | Hamburg | eigener FTP-Pfad |
|
|
||||||
| 103 | B | Berlin | eigener FTP-Pfad |
|
|
||||||
| 104 | H | Hannover | eigener FTP-Pfad |
|
|
||||||
| 105 | F | Frankfurt | eigener FTP-Pfad |
|
|
||||||
| 106 | DD | Dresden | eigener FTP-Pfad |
|
|
||||||
| 107 | E | Essen | eigener FTP-Pfad |
|
|
||||||
| 108 | L | Leipzig | eigener FTP-Pfad |
|
|
||||||
| 109 | M | Muenchen | eigener FTP-Pfad |
|
|
||||||
| 110 | N | Nuernberg | eigener FTP-Pfad |
|
|
||||||
| 111 | S | Stuttgart | eigener FTP-Pfad |
|
|
||||||
| 112 | K | Koeln | eigener FTP-Pfad |
|
|
||||||
| 203 | LG | Logistics | eigener FTP-Pfad |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Besondere Niederlassungen
|
|
||||||
|
|
||||||
### 7.1 Stuttgart (hq_id=111) -- Eigenstaendige Marke
|
|
||||||
|
|
||||||
Stuttgart operiert unter der Marke **ES-Logistic GmbH** mit:
|
|
||||||
- Eigener Domain: `es-l.eu`
|
|
||||||
- Eigenem Logo: `esl_logo.png` (342x93 px)
|
|
||||||
- Eigener Absenderadresse: `info@es-l.eu`
|
|
||||||
- Eigenem Mail-Footer mit Reichenbach-Adresse
|
|
||||||
- Abweichende EID-Praefixe in SQL-Template: `ES` statt Stadtkuerzel
|
|
||||||
|
|
||||||
### 7.2 Koeln (hq_id=112) -- Abweichende Nummernkreise
|
|
||||||
|
|
||||||
- `CR_EID_GENERATION = HTK89999` (alle anderen: 28800)
|
|
||||||
- `CS_EID_GENERATION = HTK79999` (alle anderen: {Prefix}64999)
|
|
||||||
- `CSC_ID_PAYER_EXTERN = 55241` (alle anderen: 44539-44549)
|
|
||||||
|
|
||||||
### 7.3 Leipzig (hq_id=108) -- Eingeschraenkte Funktionen
|
|
||||||
|
|
||||||
- PDA-Lokalisierung deaktiviert
|
|
||||||
- Rechnungstext fuer Kuriere nicht sichtbar
|
|
||||||
- Absender-Mail mit Display-Name-Format
|
|
||||||
|
|
||||||
### 7.4 Logistics/Zentrale (hq_id=203) -- Shared mit Berlin
|
|
||||||
|
|
||||||
- Teilt Kostenstellen mit Berlin (CSC_ID_PAYER_CASH, CSC_ID_PAYER_EXTERN)
|
|
||||||
- PDA-Lokalisierung deaktiviert
|
|
||||||
- Dient als zentrale Verwaltungs-/Logistikeinheit
|
|
||||||
|
|
||||||
### 7.5 Bremen (hq_id=101) -- Meiste Sonderkonfiguration
|
|
||||||
|
|
||||||
- Kundenprovision aktiv (14%)
|
|
||||||
- MODE_COPY_JOB_DISPOINFO aktiviert (einzige NL)
|
|
||||||
- Stationen im Kurier-Rechnungsmodul aktiv
|
|
||||||
- Rechnungstext fuer Kuriere nicht sichtbar
|
|
||||||
- Stark eingeschraenktes PDA-Fenster (nur 08:00-09:00)
|
|
||||||
- Eigene Marketingtexte (MAIL_TEXT_COMPLETION_2/3/4)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Mehrfach-Niederlassungszugriff
|
|
||||||
|
|
||||||
Mitarbeiter koennen ueber den Parameter `HEADQUARTERS_MULTIPLE_ACCESS_EMPLOYEES` Zugriff auf mehrere Niederlassungen erhalten. Die Konfiguration erfolgt als Pipe-getrennte Liste von Mitarbeiter-IDs:
|
|
||||||
|
|
||||||
```
|
|
||||||
empId1|empId2|empId3
|
|
||||||
```
|
|
||||||
|
|
||||||
Die Pruefung erfolgt in `include/auth.inc.php` (Zeile 236).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Zusammenfassung der Abweichungen
|
|
||||||
|
|
||||||
### Matrix: Funktionsunterschiede
|
|
||||||
|
|
||||||
| Funktion | HB | HH | B | H | F | DD | E | L | M | N | S | K | LG |
|
|
||||||
|----------|----|----|---|---|---|----|---|---|---|---|---|---|----|
|
|
||||||
| PDA-Lokalisierung | eingeschraenkt | eingeschraenkt | voll | voll | voll | voll | voll | **AUS** | voll | voll | voll | voll | **AUS** |
|
|
||||||
| Max. Tourstationen | 19 | **49** | 19 | 19 | **49** | 19 | 19 | 19 | **49** | 19 | 19 | 19 | 19 |
|
|
||||||
| Kundenprovision | **14%** | - | - | **10%** | - | - | - | - | - | - | - | - | - |
|
|
||||||
| Kurier-Rechnung Stationen | **ja** | - | **ja** | - | - | - | - | - | - | - | - | - | - |
|
|
||||||
| Kurier sieht Rechnungstext | **nein** | ja | ja | ja | ja | **nein** | ja | **nein** | ja | ja | ja | ja | ja |
|
|
||||||
| Dispoinfo beim Kopieren | **ignoriert** | kopiert | kopiert | kopiert | kopiert | kopiert | kopiert | kopiert | kopiert | kopiert | kopiert | kopiert | kopiert |
|
|
||||||
| Eigene Marke | HT | HT | HT | HT | HT | HT | HT | HT | HT | HT | **ESL** | HT | HT |
|
|
||||||
| EID-Nummernkreis | Standard | Standard | Standard | Standard | Standard | Standard | Standard | Standard | Standard | Standard | Standard | **abweichend** | Standard |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Dokumentation erstellt am 24.03.2026 auf Basis der Codebasis und SQL-Dumps im Verzeichnis `/Users/svencarstensen/Downloads/html`*
|
|
||||||
@@ -42,6 +42,16 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-mail</artifactId>
|
<artifactId>spring-boot-starter-mail</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-net</groupId>
|
||||||
|
<artifactId>commons-net</artifactId>
|
||||||
|
<version>3.11.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.mwiede</groupId>
|
||||||
|
<artifactId>jsch</artifactId>
|
||||||
|
<version>0.2.20</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.librepdf</groupId>
|
<groupId>com.github.librepdf</groupId>
|
||||||
<artifactId>openpdf</artifactId>
|
<artifactId>openpdf</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package de.votian.services.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "votian.scheduling")
|
||||||
|
public class LegacySchedulingProperties {
|
||||||
|
|
||||||
|
private String zone = "Europe/Berlin";
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String authSessionCleanupCron = "0 */5 * * * *";
|
||||||
|
private String courierAutoLogoutCron = "0 */30 * * * *";
|
||||||
|
private long courierAutoLogoutHeadquartersId = 3L;
|
||||||
|
private String acceptanceProtocolMailCron = "0 */5 * * * *";
|
||||||
|
private String acceptanceProtocolLetterCron = "30 */5 * * * *";
|
||||||
|
private int acceptanceProtocolMailLookbackDays = 1;
|
||||||
|
private int acceptanceProtocolLetterLookbackDays = 3;
|
||||||
|
private String acceptanceProtocolLetterFtpProfile = "MPS1";
|
||||||
|
|
||||||
|
public String getZone() {
|
||||||
|
return zone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setZone(String zone) {
|
||||||
|
this.zone = zone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthSessionCleanupCron() {
|
||||||
|
return authSessionCleanupCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthSessionCleanupCron(String authSessionCleanupCron) {
|
||||||
|
this.authSessionCleanupCron = authSessionCleanupCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCourierAutoLogoutCron() {
|
||||||
|
return courierAutoLogoutCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCourierAutoLogoutCron(String courierAutoLogoutCron) {
|
||||||
|
this.courierAutoLogoutCron = courierAutoLogoutCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCourierAutoLogoutHeadquartersId() {
|
||||||
|
return courierAutoLogoutHeadquartersId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCourierAutoLogoutHeadquartersId(long courierAutoLogoutHeadquartersId) {
|
||||||
|
this.courierAutoLogoutHeadquartersId = courierAutoLogoutHeadquartersId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAcceptanceProtocolMailCron() {
|
||||||
|
return acceptanceProtocolMailCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcceptanceProtocolMailCron(String acceptanceProtocolMailCron) {
|
||||||
|
this.acceptanceProtocolMailCron = acceptanceProtocolMailCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAcceptanceProtocolLetterCron() {
|
||||||
|
return acceptanceProtocolLetterCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcceptanceProtocolLetterCron(String acceptanceProtocolLetterCron) {
|
||||||
|
this.acceptanceProtocolLetterCron = acceptanceProtocolLetterCron;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAcceptanceProtocolMailLookbackDays() {
|
||||||
|
return acceptanceProtocolMailLookbackDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcceptanceProtocolMailLookbackDays(int acceptanceProtocolMailLookbackDays) {
|
||||||
|
this.acceptanceProtocolMailLookbackDays = acceptanceProtocolMailLookbackDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAcceptanceProtocolLetterLookbackDays() {
|
||||||
|
return acceptanceProtocolLetterLookbackDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcceptanceProtocolLetterLookbackDays(int acceptanceProtocolLetterLookbackDays) {
|
||||||
|
this.acceptanceProtocolLetterLookbackDays = acceptanceProtocolLetterLookbackDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAcceptanceProtocolLetterFtpProfile() {
|
||||||
|
return acceptanceProtocolLetterFtpProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAcceptanceProtocolLetterFtpProfile(String acceptanceProtocolLetterFtpProfile) {
|
||||||
|
this.acceptanceProtocolLetterFtpProfile = acceptanceProtocolLetterFtpProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.votian.services.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableScheduling
|
||||||
|
@EnableConfigurationProperties(LegacySchedulingProperties.class)
|
||||||
|
public class SchedulingConfig {
|
||||||
|
}
|
||||||
@@ -242,6 +242,32 @@ public class JobController {
|
|||||||
return ResponseEntity.ok(jobService.findTodaysJobs());
|
return ResponseEntity.ok(jobService.findTodaysJobs());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/today/count")
|
||||||
|
public ResponseEntity<Map<String, Long>> countTodaysJobs(Authentication authentication) {
|
||||||
|
UserSessionDto user = sessionUser(authentication);
|
||||||
|
if (isCourierUser(user)) {
|
||||||
|
long count = jobService.findByCourierId(user.getCourierId()).stream()
|
||||||
|
.filter(job -> matchesDateRange(job.getOrderTime(),
|
||||||
|
java.time.LocalDate.now().atStartOfDay(),
|
||||||
|
java.time.LocalDate.now().atTime(23, 59, 59)))
|
||||||
|
.count();
|
||||||
|
return ResponseEntity.ok(Map.of("count", count));
|
||||||
|
}
|
||||||
|
if (!accessControlService.canViewJobs(user)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
if (isCustomerUser(user)) {
|
||||||
|
long count = jobService.findByPayerCostCenters(user.getAccessibleCostCenterIds()).stream()
|
||||||
|
.filter(job -> matchesDateRange(job.getOrderTime(),
|
||||||
|
java.time.LocalDate.now().atStartOfDay(),
|
||||||
|
java.time.LocalDate.now().atTime(23, 59, 59)))
|
||||||
|
.filter(job -> user.getHeadquartersId() == null || user.getHeadquartersId().equals(job.getHeadquartersId()))
|
||||||
|
.count();
|
||||||
|
return ResponseEntity.ok(Map.of("count", count));
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(Map.of("count", jobService.countTodaysJobs(user != null ? user.getHeadquartersId() : null)));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<Job> create(@RequestBody JobCreateRequest request, Authentication authentication) {
|
public ResponseEntity<Job> create(@RequestBody JobCreateRequest request, Authentication authentication) {
|
||||||
UserSessionDto user = sessionUser(authentication);
|
UserSessionDto user = sessionUser(authentication);
|
||||||
|
|||||||
@@ -69,9 +69,11 @@ public class Company {
|
|||||||
private String logo;
|
private String logo;
|
||||||
|
|
||||||
@Column(name = "cmp_logo_width")
|
@Column(name = "cmp_logo_width")
|
||||||
|
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||||
private Integer logoWidth;
|
private Integer logoWidth;
|
||||||
|
|
||||||
@Column(name = "cmp_logo_height")
|
@Column(name = "cmp_logo_height")
|
||||||
|
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||||
private Integer logoHeight;
|
private Integer logoHeight;
|
||||||
|
|
||||||
@Column(name = "cmp_modify_status")
|
@Column(name = "cmp_modify_status")
|
||||||
|
|||||||
@@ -23,18 +23,12 @@ public class CostCenter {
|
|||||||
@Column(name = "csc_path")
|
@Column(name = "csc_path")
|
||||||
private String path;
|
private String path;
|
||||||
|
|
||||||
@Column(name = "csc_id_payer")
|
|
||||||
private Long payerId;
|
|
||||||
|
|
||||||
@Column(name = "csc_visible")
|
@Column(name = "csc_visible")
|
||||||
private String visible;
|
private String visible;
|
||||||
|
|
||||||
@Column(name = "csc_is_extern")
|
@Column(name = "csc_is_extern")
|
||||||
private String isExtern;
|
private String isExtern;
|
||||||
|
|
||||||
@Column(name = "emp_id_related")
|
|
||||||
private Long relatedEmployeeId;
|
|
||||||
|
|
||||||
public CostCenter() {}
|
public CostCenter() {}
|
||||||
|
|
||||||
public Long getId() { return id; }
|
public Long getId() { return id; }
|
||||||
@@ -47,12 +41,8 @@ public class CostCenter {
|
|||||||
public void setParentId(Long parentId) { this.parentId = parentId; }
|
public void setParentId(Long parentId) { this.parentId = parentId; }
|
||||||
public String getPath() { return path; }
|
public String getPath() { return path; }
|
||||||
public void setPath(String path) { this.path = path; }
|
public void setPath(String path) { this.path = path; }
|
||||||
public Long getPayerId() { return payerId; }
|
|
||||||
public void setPayerId(Long payerId) { this.payerId = payerId; }
|
|
||||||
public String getVisible() { return visible; }
|
public String getVisible() { return visible; }
|
||||||
public void setVisible(String visible) { this.visible = visible; }
|
public void setVisible(String visible) { this.visible = visible; }
|
||||||
public String getIsExtern() { return isExtern; }
|
public String getIsExtern() { return isExtern; }
|
||||||
public void setIsExtern(String isExtern) { this.isExtern = isExtern; }
|
public void setIsExtern(String isExtern) { this.isExtern = isExtern; }
|
||||||
public Long getRelatedEmployeeId() { return relatedEmployeeId; }
|
|
||||||
public void setRelatedEmployeeId(Long relatedEmployeeId) { this.relatedEmployeeId = relatedEmployeeId; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.votian.services.entity;
|
package de.votian.services.entity;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.Formula;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "employee")
|
@Table(name = "employee")
|
||||||
@@ -14,7 +15,7 @@ public class Employee {
|
|||||||
@Column(name = "usr_id")
|
@Column(name = "usr_id")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
@Column(name = "hq_id")
|
@Formula("(select u.hq_id from user u where u.usr_id = usr_id)")
|
||||||
private Long headquartersId;
|
private Long headquartersId;
|
||||||
|
|
||||||
@Column(name = "csc_id")
|
@Column(name = "csc_id")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import jakarta.persistence.Table;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "appointment", schema = "phoenix_group")
|
@Table(name = "appointment", catalog = "phoenix_group")
|
||||||
public class GroupwareAppointment {
|
public class GroupwareAppointment {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ public class Job {
|
|||||||
private Integer longhaul;
|
private Integer longhaul;
|
||||||
|
|
||||||
@Column(name = "jb_service")
|
@Column(name = "jb_service")
|
||||||
|
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||||
private Integer service;
|
private Integer service;
|
||||||
|
|
||||||
@Column(name = "vht_id")
|
@Column(name = "vht_id")
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package de.votian.services.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.AttributeConverter;
|
||||||
|
import jakarta.persistence.Converter;
|
||||||
|
|
||||||
|
@Converter
|
||||||
|
public class LegacyBlankableIntegerStringConverter implements AttributeConverter<Integer, String> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String convertToDatabaseColumn(Integer attribute) {
|
||||||
|
return attribute != null ? attribute.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer convertToEntityAttribute(String dbData) {
|
||||||
|
if (dbData == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = dbData.trim();
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Integer.valueOf(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,15 @@ public interface CostCenterRepository extends JpaRepository<CostCenter, Long> {
|
|||||||
@Query("SELECT csc FROM CostCenter csc WHERE csc.customerId = :csId AND csc.visible = '1'")
|
@Query("SELECT csc FROM CostCenter csc WHERE csc.customerId = :csId AND csc.visible = '1'")
|
||||||
List<CostCenter> findVisibleByCustomerId(@Param("csId") Long csId);
|
List<CostCenter> findVisibleByCustomerId(@Param("csId") Long csId);
|
||||||
|
|
||||||
|
@Query(value = "SELECT c.* " +
|
||||||
|
"FROM costcenter c " +
|
||||||
|
"JOIN customer cs ON cs.cs_id = c.cs_id " +
|
||||||
|
"WHERE cs.hq_id = :hqId " +
|
||||||
|
" AND c.csc_visible = 1 " +
|
||||||
|
" AND COALESCE(c.csc_is_extern, 0) <> 1",
|
||||||
|
nativeQuery = true)
|
||||||
|
List<CostCenter> findVisibleNonExternByHeadquartersId(@Param("hqId") Long hqId);
|
||||||
|
|
||||||
@Query("SELECT csc FROM CostCenter csc WHERE csc.path LIKE %:pathFragment% AND csc.customerId = :csId")
|
@Query("SELECT csc FROM CostCenter csc WHERE csc.path LIKE %:pathFragment% AND csc.customerId = :csId")
|
||||||
List<CostCenter> findByPathContainingAndCustomerId(@Param("pathFragment") String pathFragment, @Param("csId") Long csId);
|
List<CostCenter> findByPathContainingAndCustomerId(@Param("pathFragment") String pathFragment, @Param("csId") Long csId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ public interface JobRepository extends JpaRepository<Job, Long> {
|
|||||||
@Query("SELECT j FROM Job j WHERE j.orderTime >= CURRENT_DATE")
|
@Query("SELECT j FROM Job j WHERE j.orderTime >= CURRENT_DATE")
|
||||||
List<Job> findTodaysJobs();
|
List<Job> findTodaysJobs();
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(j) FROM Job j WHERE j.headquartersId = :hqId AND j.orderTime >= :from AND j.orderTime < :to")
|
||||||
|
long countByOrderTimeBetweenAndHq(@Param("hqId") Long hqId,
|
||||||
|
@Param("from") LocalDateTime from,
|
||||||
|
@Param("to") LocalDateTime to);
|
||||||
|
|
||||||
@Query("SELECT j FROM Job j WHERE j.copyPermanentId IS NOT NULL AND j.orderTime > :since " +
|
@Query("SELECT j FROM Job j WHERE j.copyPermanentId IS NOT NULL AND j.orderTime > :since " +
|
||||||
"AND (j.costCenterPayerId IN :cscIds OR j.costCenterPayerCashId IN :cscIds) ORDER BY j.id")
|
"AND (j.costCenterPayerId IN :cscIds OR j.costCenterPayerCashId IN :cscIds) ORDER BY j.id")
|
||||||
List<Job> findPermanentCopies(@Param("since") LocalDateTime since, @Param("cscIds") List<Long> cscIds);
|
List<Job> findPermanentCopies(@Param("since") LocalDateTime since, @Param("cscIds") List<Long> cscIds);
|
||||||
|
|||||||
@@ -36,10 +36,11 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
|||||||
List<User> findByTypeAndHeadquartersIdOrderByNameAscFirstnameAsc(@Param("type") Integer type,
|
List<User> findByTypeAndHeadquartersIdOrderByNameAscFirstnameAsc(@Param("type") Integer type,
|
||||||
@Param("hqId") Long headquartersId);
|
@Param("hqId") Long headquartersId);
|
||||||
|
|
||||||
@Query("SELECT DISTINCT hq.mnemonic, u.name, u.firstname, u.type " +
|
@Query(value = "SELECT DISTINCT hq.hq_mnemonic, u.usr_name, u.usr_firstname, u.usr_type " +
|
||||||
"FROM User u JOIN Headquarters hq ON u.headquartersId = hq.id " +
|
"FROM user u JOIN headquarters hq ON u.hq_id = hq.hq_id " +
|
||||||
"WHERE FUNCTION('RIGHT', u.birthdate, 5) = FUNCTION('RIGHT', CURRENT_DATE, 5) " +
|
"WHERE DATE_FORMAT(u.usr_birthdate, '%m-%d') = DATE_FORMAT(CURRENT_DATE, '%m-%d') " +
|
||||||
"ORDER BY u.type, u.name")
|
"ORDER BY u.usr_type, u.usr_name",
|
||||||
|
nativeQuery = true)
|
||||||
List<Object[]> findBirthdaysToday();
|
List<Object[]> findBirthdaysToday();
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
|
|||||||
@@ -57,6 +57,33 @@ public class AuthSessionService {
|
|||||||
return Optional.of(state.user());
|
return Optional.of(state.user());
|
||||||
}
|
}
|
||||||
|
|
||||||
private record SessionState(UserSessionDto user, Instant expiresAt) { }
|
public CleanupResult cleanupExpiredSessions() {
|
||||||
private record PendingTotpState(UserSessionDto user, Instant expiresAt) { }
|
Instant now = Instant.now();
|
||||||
|
int expiredAuthenticated = removeExpiredEntries(authSessions, now);
|
||||||
|
int expiredPendingTotp = removeExpiredEntries(pendingTotpSessions, now);
|
||||||
|
return new CleanupResult(expiredAuthenticated, expiredPendingTotp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CleanupResult(int expiredAuthenticatedSessions, int expiredPendingTotpSessions) {
|
||||||
|
public int totalExpiredSessions() {
|
||||||
|
return expiredAuthenticatedSessions + expiredPendingTotpSessions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T extends ExpiringState> int removeExpiredEntries(Map<String, T> sessions, Instant now) {
|
||||||
|
int removed = 0;
|
||||||
|
for (Map.Entry<String, T> entry : sessions.entrySet()) {
|
||||||
|
if (entry.getValue().expiresAt().isBefore(now) && sessions.remove(entry.getKey(), entry.getValue())) {
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface ExpiringState permits SessionState, PendingTotpState {
|
||||||
|
Instant expiresAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SessionState(UserSessionDto user, Instant expiresAt) implements ExpiringState { }
|
||||||
|
private record PendingTotpState(UserSessionDto user, Instant expiresAt) implements ExpiringState { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,453 @@
|
|||||||
|
package de.votian.services.service;
|
||||||
|
|
||||||
|
import de.votian.services.config.LegacySchedulingProperties;
|
||||||
|
import de.votian.services.config.ParameterKeys;
|
||||||
|
import de.votian.services.dto.JobAcceptanceProtocolDto;
|
||||||
|
import de.votian.services.entity.CostCenter;
|
||||||
|
import de.votian.services.entity.CostCenterAddress;
|
||||||
|
import de.votian.services.entity.Customer;
|
||||||
|
import de.votian.services.entity.GenericDataContainer;
|
||||||
|
import de.votian.services.entity.Job;
|
||||||
|
import de.votian.services.repository.CostCenterAddressRepository;
|
||||||
|
import de.votian.services.repository.CostCenterRepository;
|
||||||
|
import de.votian.services.repository.CustomerRepository;
|
||||||
|
import de.votian.services.repository.GenericDataContainerRepository;
|
||||||
|
import de.votian.services.repository.JobRepository;
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.mail.MailException;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AcceptanceProtocolAutomationService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AcceptanceProtocolAutomationService.class);
|
||||||
|
private static final String JOB_OBJECT_TYPE = "jb";
|
||||||
|
private static final String MAIL_FIELD = "mail_srv_acc_prot";
|
||||||
|
private static final String LETTER_FIELD = "letter_srv_acc_prot";
|
||||||
|
private static final String IN_PROGRESS = "SEND_IN_PROGRESS";
|
||||||
|
private static final String MAIL_OK = "MAIL_SENT=OK";
|
||||||
|
private static final String MAIL_NOT_OK = "MAIL_SENT=NOT_OK";
|
||||||
|
private static final String LETTER_OK = "LETTER_SENT=OK";
|
||||||
|
private static final String LETTER_NOT_OK = "LETTER_SENT=NOT_OK";
|
||||||
|
private static final String PDF_BASE_NAME = "ABNAHMEPROTOKOLL";
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final JobRepository jobRepository;
|
||||||
|
private final CostCenterRepository costCenterRepository;
|
||||||
|
private final CustomerRepository customerRepository;
|
||||||
|
private final CostCenterAddressRepository costCenterAddressRepository;
|
||||||
|
private final GenericDataContainerRepository genericDataContainerRepository;
|
||||||
|
private final JobAcceptanceProtocolService jobAcceptanceProtocolService;
|
||||||
|
private final ParameterService parameterService;
|
||||||
|
private final LegacyFileTransferService legacyFileTransferService;
|
||||||
|
private final ObjectProvider<JavaMailSender> mailSenderProvider;
|
||||||
|
private final LegacySchedulingProperties schedulingProperties;
|
||||||
|
private final AtomicBoolean mailRunning = new AtomicBoolean();
|
||||||
|
private final AtomicBoolean letterRunning = new AtomicBoolean();
|
||||||
|
|
||||||
|
public AcceptanceProtocolAutomationService(JdbcTemplate jdbcTemplate,
|
||||||
|
JobRepository jobRepository,
|
||||||
|
CostCenterRepository costCenterRepository,
|
||||||
|
CustomerRepository customerRepository,
|
||||||
|
CostCenterAddressRepository costCenterAddressRepository,
|
||||||
|
GenericDataContainerRepository genericDataContainerRepository,
|
||||||
|
JobAcceptanceProtocolService jobAcceptanceProtocolService,
|
||||||
|
ParameterService parameterService,
|
||||||
|
LegacyFileTransferService legacyFileTransferService,
|
||||||
|
ObjectProvider<JavaMailSender> mailSenderProvider,
|
||||||
|
LegacySchedulingProperties schedulingProperties) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.jobRepository = jobRepository;
|
||||||
|
this.costCenterRepository = costCenterRepository;
|
||||||
|
this.customerRepository = customerRepository;
|
||||||
|
this.costCenterAddressRepository = costCenterAddressRepository;
|
||||||
|
this.genericDataContainerRepository = genericDataContainerRepository;
|
||||||
|
this.jobAcceptanceProtocolService = jobAcceptanceProtocolService;
|
||||||
|
this.parameterService = parameterService;
|
||||||
|
this.legacyFileTransferService = legacyFileTransferService;
|
||||||
|
this.mailSenderProvider = mailSenderProvider;
|
||||||
|
this.schedulingProperties = schedulingProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int dispatchPendingMailProtocols() {
|
||||||
|
if (!isCronEnabled() || !mailRunning.compareAndSet(false, true)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int processed = 0;
|
||||||
|
for (Long jobId : loadMailCandidates()) {
|
||||||
|
if (dispatchMail(jobId)) {
|
||||||
|
processed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (processed > 0) {
|
||||||
|
log.info("Acceptance protocol mail automation processed {} job(s).", processed);
|
||||||
|
}
|
||||||
|
return processed;
|
||||||
|
} finally {
|
||||||
|
mailRunning.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int dispatchPendingLetterProtocols() {
|
||||||
|
if (!isCronEnabled() || !letterRunning.compareAndSet(false, true)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int processed = 0;
|
||||||
|
for (Long jobId : loadLetterCandidates()) {
|
||||||
|
if (dispatchLetter(jobId)) {
|
||||||
|
processed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (processed > 0) {
|
||||||
|
log.info("Acceptance protocol letter automation processed {} job(s).", processed);
|
||||||
|
}
|
||||||
|
return processed;
|
||||||
|
} finally {
|
||||||
|
letterRunning.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean dispatchMail(Long jobId) {
|
||||||
|
DispatchContext context = resolveContext(jobId);
|
||||||
|
if (context == null || !context.protocol().isDataAvailable() || !context.protocol().isSignatureAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MailRecipients recipients = resolveRecipients(context);
|
||||||
|
if (recipients.to().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!markInProgress(jobId, MAIL_FIELD)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] pdf = jobAcceptanceProtocolService.createPdf(jobId);
|
||||||
|
sendMail(context, recipients, pdf);
|
||||||
|
updateDispatchState(jobId, MAIL_FIELD, MAIL_OK);
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
log.warn("Acceptance protocol mail dispatch failed for job {}: {}", jobId, exception.getMessage());
|
||||||
|
updateDispatchState(jobId, MAIL_FIELD, MAIL_NOT_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean dispatchLetter(Long jobId) {
|
||||||
|
DispatchContext context = resolveContext(jobId);
|
||||||
|
if (context == null || !context.protocol().isDataAvailable() || !context.protocol().isSignatureAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!markInProgress(jobId, LETTER_FIELD)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] pdf = jobAcceptanceProtocolService.createPdf(jobId);
|
||||||
|
String fileName = PDF_BASE_NAME + "_" + jobId + ".pdf";
|
||||||
|
legacyFileTransferService.upload(
|
||||||
|
schedulingProperties.getAcceptanceProtocolLetterFtpProfile(),
|
||||||
|
fileName,
|
||||||
|
pdf
|
||||||
|
);
|
||||||
|
updateDispatchState(jobId, LETTER_FIELD, LETTER_OK);
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
log.warn("Acceptance protocol letter dispatch failed for job {}: {}", jobId, exception.getMessage());
|
||||||
|
updateDispatchState(jobId, LETTER_FIELD, LETTER_NOT_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DispatchContext resolveContext(Long jobId) {
|
||||||
|
Job job = jobRepository.findById(Objects.requireNonNull(jobId)).orElse(null);
|
||||||
|
if (job == null || job.getHeadquartersId() == null || job.getCostCenterRelatedId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CostCenter relatedCostCenter = costCenterRepository.findById(job.getCostCenterRelatedId()).orElse(null);
|
||||||
|
if (relatedCostCenter == null || relatedCostCenter.getCustomerId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Customer customer = customerRepository.findById(relatedCostCenter.getCustomerId()).orElse(null);
|
||||||
|
if (customer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobAcceptanceProtocolDto protocol = jobAcceptanceProtocolService.getProtocol(jobId);
|
||||||
|
Long groupId = extractFirstNumericGroup(customer.getGroup());
|
||||||
|
return new DispatchContext(job, customer, protocol, groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MailRecipients resolveRecipients(DispatchContext context) {
|
||||||
|
long headquartersId = context.job().getHeadquartersId();
|
||||||
|
Long customerId = context.customer().getId();
|
||||||
|
Long groupId = context.groupId();
|
||||||
|
|
||||||
|
String mailTo = firstNonBlank(
|
||||||
|
parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO + "_CS", customerId, headquartersId, 0L, true),
|
||||||
|
groupId != null
|
||||||
|
? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO + "_GRP", groupId, headquartersId, 0L, true)
|
||||||
|
: "",
|
||||||
|
parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_TO, headquartersId)
|
||||||
|
);
|
||||||
|
String mailCc = firstNonBlank(
|
||||||
|
parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC + "_CS", customerId, headquartersId, 0L, true),
|
||||||
|
groupId != null
|
||||||
|
? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC + "_GRP", groupId, headquartersId, 0L, true)
|
||||||
|
: "",
|
||||||
|
parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_CC, headquartersId)
|
||||||
|
);
|
||||||
|
String mailBcc = firstNonBlank(
|
||||||
|
parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC + "_CS", customerId, headquartersId, 0L, true),
|
||||||
|
groupId != null
|
||||||
|
? parameterService.getObjectBasedParameterValue(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC + "_GRP", groupId, headquartersId, 0L, true)
|
||||||
|
: "",
|
||||||
|
parameterService.getParameterValueWithFallback(ParameterKeys.AUTOMAILER_ACCEPTANCE_PROTOCOL_MAIL_BCC, headquartersId)
|
||||||
|
);
|
||||||
|
|
||||||
|
LinkedHashSet<String> toRecipients = splitRecipients(mailTo);
|
||||||
|
resolvePayerMailAddress(context.job().getCostCenterPayerId()).ifPresent(toRecipients::add);
|
||||||
|
return new MailRecipients(
|
||||||
|
List.copyOf(toRecipients),
|
||||||
|
List.copyOf(splitRecipients(mailCc)),
|
||||||
|
List.copyOf(splitRecipients(mailBcc))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> resolvePayerMailAddress(Long payerCostCenterId) {
|
||||||
|
if (payerCostCenterId == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int addressTypeId : List.of(4, 2, 3, 1)) {
|
||||||
|
Optional<String> address = costCenterAddressRepository.findByCostCenterIdAndAddressTypeId(payerCostCenterId, addressTypeId)
|
||||||
|
.map(CostCenterAddress::getEmail)
|
||||||
|
.map(this::normalizeRecipient)
|
||||||
|
.filter(value -> !value.isBlank());
|
||||||
|
if (address.isPresent()) {
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMail(DispatchContext context, MailRecipients recipients, byte[] pdf) {
|
||||||
|
JavaMailSender mailSender = mailSenderProvider.getIfAvailable();
|
||||||
|
if (mailSender == null) {
|
||||||
|
throw new IllegalStateException("Kein JavaMailSender konfiguriert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
MimeMessage message = mailSender.createMimeMessage();
|
||||||
|
try {
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());
|
||||||
|
helper.setTo(recipients.to().toArray(String[]::new));
|
||||||
|
if (!recipients.cc().isEmpty()) {
|
||||||
|
helper.setCc(recipients.cc().toArray(String[]::new));
|
||||||
|
}
|
||||||
|
if (!recipients.bcc().isEmpty()) {
|
||||||
|
helper.setBcc(recipients.bcc().toArray(String[]::new));
|
||||||
|
}
|
||||||
|
helper.setSubject(PDF_BASE_NAME + " (" + context.job().getId() + ")");
|
||||||
|
helper.setText(buildMailBody(context), false);
|
||||||
|
|
||||||
|
String senderAddress = parameterService.getParameterValueWithFallback(ParameterKeys.MAIL_SENDER_ADDRESS, context.job().getHeadquartersId());
|
||||||
|
if (!senderAddress.isBlank()) {
|
||||||
|
helper.setFrom(senderAddress);
|
||||||
|
helper.setReplyTo(senderAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.addAttachment(PDF_BASE_NAME + "_" + context.job().getId() + ".pdf", new ByteArrayResource(pdf));
|
||||||
|
} catch (MessagingException exception) {
|
||||||
|
throw new IllegalStateException("Mail konnte nicht vorbereitet werden.", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mailSender.send(message);
|
||||||
|
} catch (MailException exception) {
|
||||||
|
throw new IllegalStateException("Mailversand fehlgeschlagen.", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildMailBody(DispatchContext context) {
|
||||||
|
StringBuilder body = new StringBuilder();
|
||||||
|
body.append("Sehr geehrte Damen und Herren,").append("\n\n");
|
||||||
|
body.append("im Anhang erhalten Sie das Abnahmeprotokoll zum Auftrag ")
|
||||||
|
.append(context.job().getId())
|
||||||
|
.append('.')
|
||||||
|
.append("\n\n");
|
||||||
|
if (context.protocol().getProtocolServiceLabel() != null && !context.protocol().getProtocolServiceLabel().isBlank()) {
|
||||||
|
body.append("Protokoll-Service: ")
|
||||||
|
.append(context.protocol().getProtocolServiceLabel())
|
||||||
|
.append("\n");
|
||||||
|
}
|
||||||
|
body.append("\nMit freundlichen Gruessen");
|
||||||
|
return body.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean markInProgress(Long jobId, String fieldName) {
|
||||||
|
Optional<GenericDataContainer> existing = genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName);
|
||||||
|
if (existing.isPresent() && contains(existing.get().getContext(), IN_PROGRESS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GenericDataContainer container = existing.orElseGet(GenericDataContainer::new);
|
||||||
|
container.setObjectType(JOB_OBJECT_TYPE);
|
||||||
|
container.setObjectId(jobId);
|
||||||
|
container.setFieldName(fieldName);
|
||||||
|
container.setContent(existing.map(GenericDataContainer::getContent).orElse(""));
|
||||||
|
container.setContext(IN_PROGRESS);
|
||||||
|
genericDataContainerRepository.save(container);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDispatchState(Long jobId, String fieldName, String context) {
|
||||||
|
GenericDataContainer container = genericDataContainerRepository.findByTypeAndIdAndField(JOB_OBJECT_TYPE, jobId, fieldName)
|
||||||
|
.orElseGet(GenericDataContainer::new);
|
||||||
|
container.setObjectType(JOB_OBJECT_TYPE);
|
||||||
|
container.setObjectId(jobId);
|
||||||
|
container.setFieldName(fieldName);
|
||||||
|
container.setContent(container.getContent() != null ? container.getContent() : "");
|
||||||
|
container.setContext(context);
|
||||||
|
genericDataContainerRepository.save(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> loadMailCandidates() {
|
||||||
|
return jdbcTemplate.queryForList(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT jb.jb_id
|
||||||
|
FROM job AS jb
|
||||||
|
JOIN costcenteraddress AS cscad ON cscad.csc_id = jb.csc_id_payer AND cscad.adt_id = 2
|
||||||
|
LEFT JOIN genericdatacontainer AS gdc
|
||||||
|
ON gdc.gdc_obj_type = 'jb'
|
||||||
|
AND gdc.gdc_obj_id = jb.jb_id
|
||||||
|
AND gdc.gdc_gen_fieldname = ?
|
||||||
|
WHERE jb.jb_finishtime > ?
|
||||||
|
AND (jb.jb_storno IS NULL OR jb.jb_storno = '0')
|
||||||
|
AND jb.jb_status = 2
|
||||||
|
AND COALESCE(jb.jb_offer, 0) = 0
|
||||||
|
AND (jb.jb_service & 2) = 2
|
||||||
|
AND COALESCE(cscad.cscad_email, '') <> ''
|
||||||
|
AND (gdc.gdc_id IS NULL
|
||||||
|
OR (COALESCE(gdc.gdc_context, '') NOT LIKE '%MAIL_SENT=OK%'
|
||||||
|
AND COALESCE(gdc.gdc_context, '') NOT LIKE '%SEND_IN_PROGRESS%'))
|
||||||
|
ORDER BY jb.jb_id
|
||||||
|
""",
|
||||||
|
Long.class,
|
||||||
|
MAIL_FIELD,
|
||||||
|
Timestamp.valueOf(LocalDateTime.now().minusDays(Math.max(1, schedulingProperties.getAcceptanceProtocolMailLookbackDays())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> loadLetterCandidates() {
|
||||||
|
return jdbcTemplate.queryForList(
|
||||||
|
"""
|
||||||
|
SELECT DISTINCT jb.jb_id
|
||||||
|
FROM job AS jb
|
||||||
|
JOIN costcenteraddress AS cscad ON cscad.csc_id = jb.csc_id_payer AND cscad.adt_id = 2
|
||||||
|
LEFT JOIN genericdatacontainer AS gdc
|
||||||
|
ON gdc.gdc_obj_type = 'jb'
|
||||||
|
AND gdc.gdc_obj_id = jb.jb_id
|
||||||
|
AND gdc.gdc_gen_fieldname = ?
|
||||||
|
WHERE jb.jb_finishtime > ?
|
||||||
|
AND (jb.jb_storno IS NULL OR jb.jb_storno = '0')
|
||||||
|
AND jb.jb_status = 2
|
||||||
|
AND COALESCE(jb.jb_offer, 0) = 0
|
||||||
|
AND (jb.jb_service & 2) = 2
|
||||||
|
AND COALESCE(cscad.cscad_email, '') = ''
|
||||||
|
AND (gdc.gdc_id IS NULL
|
||||||
|
OR (COALESCE(gdc.gdc_context, '') NOT LIKE '%LETTER_SENT=OK%'
|
||||||
|
AND COALESCE(gdc.gdc_context, '') NOT LIKE '%SEND_IN_PROGRESS%'))
|
||||||
|
ORDER BY jb.jb_id
|
||||||
|
""",
|
||||||
|
Long.class,
|
||||||
|
LETTER_FIELD,
|
||||||
|
Timestamp.valueOf(LocalDateTime.now().minusDays(Math.max(1, schedulingProperties.getAcceptanceProtocolLetterLookbackDays())))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCronEnabled() {
|
||||||
|
return "1".equals(parameterService.getParameterValueWithFallback(ParameterKeys.GLOBAL_CRON_ENABLED, 0L))
|
||||||
|
&& "1".equals(parameterService.getParameterValueWithFallback(
|
||||||
|
ParameterKeys.CRON_SERVICE_ACCEPTANCE_PROTOCOL_ENABLED,
|
||||||
|
0L
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long extractFirstNumericGroup(String rawValue) {
|
||||||
|
if (rawValue == null || rawValue.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String token : rawValue.split(",")) {
|
||||||
|
String normalized = token != null ? token.trim() : "";
|
||||||
|
if (normalized.matches("\\d+")) {
|
||||||
|
return Long.valueOf(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
for (String value : values) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedHashSet<String> splitRecipients(String rawRecipients) {
|
||||||
|
LinkedHashSet<String> recipients = new LinkedHashSet<>();
|
||||||
|
if (rawRecipients == null || rawRecipients.isBlank()) {
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
for (String token : rawRecipients.split("[,;]")) {
|
||||||
|
String normalized = normalizeRecipient(token);
|
||||||
|
if (!normalized.isBlank()) {
|
||||||
|
recipients.add(normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRecipient(String value) {
|
||||||
|
String normalized = value != null ? value.trim() : "";
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return normalized.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean contains(String value, String token) {
|
||||||
|
return value != null && value.contains(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DispatchContext(Job job, Customer customer, JobAcceptanceProtocolDto protocol, Long groupId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MailRecipients(List<String> to, List<String> cc, List<String> bcc) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -90,6 +91,15 @@ public class JobService {
|
|||||||
return jobRepository.findTodaysJobs();
|
return jobRepository.findTodaysJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long countTodaysJobs(Long headquartersId) {
|
||||||
|
if (headquartersId == null) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
LocalDateTime from = LocalDate.now().atStartOfDay();
|
||||||
|
LocalDateTime to = from.plusDays(1);
|
||||||
|
return jobRepository.countByOrderTimeBetweenAndHq(headquartersId, from, to);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Job createJob(Job job, List<Tour> tours, List<TourArticle> articles) {
|
public Job createJob(Job job, List<Tour> tours, List<TourArticle> articles) {
|
||||||
return createJob(job, tours, articles, List.of());
|
return createJob(job, tours, articles, List.of());
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package de.votian.services.service;
|
||||||
|
|
||||||
|
import de.votian.services.config.LegacySchedulingProperties;
|
||||||
|
import de.votian.services.config.ParameterKeys;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LegacyCourierAutoLogoutService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LegacyCourierAutoLogoutService.class);
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final ParameterService parameterService;
|
||||||
|
private final LegacySchedulingProperties schedulingProperties;
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean();
|
||||||
|
|
||||||
|
public LegacyCourierAutoLogoutService(JdbcTemplate jdbcTemplate,
|
||||||
|
ParameterService parameterService,
|
||||||
|
LegacySchedulingProperties schedulingProperties) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.parameterService = parameterService;
|
||||||
|
this.schedulingProperties = schedulingProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public int logoutCouriersByAutorevokes() {
|
||||||
|
if (!running.compareAndSet(false, true)) {
|
||||||
|
log.info("Legacy courier auto logout is still running, skipping overlapping execution.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<CourierLogState> candidates = loadCandidates(schedulingProperties.getCourierAutoLogoutHeadquartersId());
|
||||||
|
int loggedOut = 0;
|
||||||
|
for (CourierLogState candidate : candidates) {
|
||||||
|
if (candidate.acceptedCount() > 0 || candidate.cancelledCount() > 0 || candidate.revokedCount() < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isOccupied(candidate.courierId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logoutCourier(candidate);
|
||||||
|
loggedOut++;
|
||||||
|
}
|
||||||
|
if (loggedOut > 0) {
|
||||||
|
log.info("Legacy courier auto logout logged out {} courier(s).", loggedOut);
|
||||||
|
}
|
||||||
|
return loggedOut;
|
||||||
|
} finally {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CourierLogState> loadCandidates(long headquartersId) {
|
||||||
|
return jdbcTemplate.query(
|
||||||
|
"""
|
||||||
|
SELECT cr.cr_id,
|
||||||
|
cr.cr_eid,
|
||||||
|
cr.hq_id,
|
||||||
|
SUM(CASE WHEN log.logo_id = 3 THEN 1 ELSE 0 END) AS accepted_count,
|
||||||
|
SUM(CASE WHEN log.logo_id = 8 THEN 1 ELSE 0 END) AS revoked_count,
|
||||||
|
SUM(CASE WHEN log.logo_id = 11 THEN 1 ELSE 0 END) AS cancelled_count
|
||||||
|
FROM courier AS cr
|
||||||
|
JOIN phoenix_log.log AS log ON log.cr_id = cr.cr_id
|
||||||
|
WHERE cr.cr_available = '1'
|
||||||
|
AND cr.hq_id = ?
|
||||||
|
AND cr.cr_locationzipcode REGEXP '^[0-9]+$'
|
||||||
|
AND log.log_createtime >= DATE_SUB(NOW(), INTERVAL 30 MINUTE)
|
||||||
|
AND log.logo_id IN (3, 8, 11)
|
||||||
|
GROUP BY cr.cr_id, cr.cr_eid, cr.hq_id
|
||||||
|
ORDER BY cr.cr_id
|
||||||
|
""",
|
||||||
|
(rs, rowNum) -> new CourierLogState(
|
||||||
|
rs.getLong("cr_id"),
|
||||||
|
rs.getString("cr_eid"),
|
||||||
|
rs.getLong("hq_id"),
|
||||||
|
rs.getInt("accepted_count"),
|
||||||
|
rs.getInt("revoked_count"),
|
||||||
|
rs.getInt("cancelled_count")
|
||||||
|
),
|
||||||
|
headquartersId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOccupied(long courierId) {
|
||||||
|
Integer matches = jdbcTemplate.queryForObject(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM courier AS cr
|
||||||
|
JOIN job AS jb ON jb.cr_id_order = cr.cr_id
|
||||||
|
JOIN tour AS tr ON tr.jb_id = jb.jb_id AND tr.tr_sort = 1
|
||||||
|
JOIN address AS ad ON tr.ad_id = ad.ad_id
|
||||||
|
LEFT JOIN serviceplz AS srvp ON ad.ad_zipcode = srvp.srvp_plz
|
||||||
|
LEFT JOIN serviceplztraveltime AS srvpt ON srvp.srvp_id = srvpt.srvp_id
|
||||||
|
WHERE cr.cr_id = ?
|
||||||
|
AND jb.jb_status IN (0, 1)
|
||||||
|
AND DATE_SUB(jb.jb_ordertime, INTERVAL GREATEST(COALESCE(srvpt.srvpt_traveltime, 0), 30) MINUTE) <= NOW()
|
||||||
|
AND (srvpt.hq_id = jb.hq_id OR srvpt.hq_id IS NULL)
|
||||||
|
""",
|
||||||
|
Integer.class,
|
||||||
|
courierId
|
||||||
|
);
|
||||||
|
return matches != null && matches > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logoutCourier(CourierLogState candidate) {
|
||||||
|
LocalDateTime now = LocalDateTime.now().withNano(0);
|
||||||
|
LocalDateTime executeUntil = now.plusMinutes(16);
|
||||||
|
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"UPDATE courier SET cr_availabletime = ?, cr_locationzipcode = ? WHERE cr_id = ?",
|
||||||
|
Timestamp.valueOf(now),
|
||||||
|
"LOGOUT",
|
||||||
|
candidate.courierId()
|
||||||
|
);
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"""
|
||||||
|
INSERT INTO phoenix_pda.commandexec
|
||||||
|
(cr_id, hq_id, cmd_command, cmde_exec_time, cmde_exec_timelimit, cmde_mode, cmde_param)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
candidate.courierId(),
|
||||||
|
candidate.headquartersId(),
|
||||||
|
"2",
|
||||||
|
Timestamp.valueOf(now),
|
||||||
|
Timestamp.valueOf(executeUntil),
|
||||||
|
"1",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("1".equals(parameterService.getParameterValueWithFallback(ParameterKeys.LOG_DB, candidate.headquartersId()))) {
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"""
|
||||||
|
INSERT INTO phoenix_log.log
|
||||||
|
(logo_id, hq_id, jb_id, usr_id, cr_id, cr_sid, cs_id, at_id, pt_id, emp_id, logo_description)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
143,
|
||||||
|
candidate.headquartersId(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
candidate.courierId(),
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"STATUS_LOGOUT=PDA-LOGOUT DURCH AUTOREVOKES"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Legacy courier auto logout logged out courier {} ({})", candidate.courierId(), candidate.courierEid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CourierLogState(long courierId,
|
||||||
|
String courierEid,
|
||||||
|
long headquartersId,
|
||||||
|
int acceptedCount,
|
||||||
|
int revokedCount,
|
||||||
|
int cancelledCount) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package de.votian.services.service;
|
||||||
|
|
||||||
|
import com.jcraft.jsch.ChannelSftp;
|
||||||
|
import com.jcraft.jsch.JSch;
|
||||||
|
import com.jcraft.jsch.JSchException;
|
||||||
|
import com.jcraft.jsch.Session;
|
||||||
|
import com.jcraft.jsch.SftpException;
|
||||||
|
import de.votian.services.config.ParameterKeys;
|
||||||
|
import org.apache.commons.net.ftp.FTP;
|
||||||
|
import org.apache.commons.net.ftp.FTPClient;
|
||||||
|
import org.apache.commons.net.ftp.FTPSClient;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LegacyFileTransferService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LegacyFileTransferService.class);
|
||||||
|
|
||||||
|
private final ParameterService parameterService;
|
||||||
|
|
||||||
|
public LegacyFileTransferService(ParameterService parameterService) {
|
||||||
|
this.parameterService = parameterService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upload(String profile, String fileName, byte[] content) {
|
||||||
|
TransferProfile transferProfile = loadProfile(profile);
|
||||||
|
switch (transferProfile.sslMode()) {
|
||||||
|
case "2" -> uploadViaSftp(transferProfile, fileName, content);
|
||||||
|
case "1" -> uploadViaFtps(transferProfile, fileName, content);
|
||||||
|
default -> uploadViaFtp(transferProfile, fileName, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransferProfile loadProfile(String profile) {
|
||||||
|
String normalizedProfile = profile != null ? profile.trim().toUpperCase() : "";
|
||||||
|
if (normalizedProfile.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("FTP-Profil fehlt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String server = loadProfileValue(ParameterKeys.FTP_SERVER_PREFIX, normalizedProfile);
|
||||||
|
String user = loadProfileValue(ParameterKeys.FTP_USER_PREFIX, normalizedProfile);
|
||||||
|
String password = loadProfileValue(ParameterKeys.FTP_PASSWORD_PREFIX, normalizedProfile);
|
||||||
|
String sslMode = blankToDefault(loadProfileValue(ParameterKeys.FTP_SSL, normalizedProfile), "0");
|
||||||
|
String remotePath = blankToDefault(loadProfileValue(ParameterKeys.FTP_REMOTEPATH, normalizedProfile), "/");
|
||||||
|
|
||||||
|
if (server.isBlank() || user.isBlank() || password.isBlank()) {
|
||||||
|
throw new IllegalStateException("FTP-Profil " + normalizedProfile + " ist unvollstaendig konfiguriert.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Endpoint endpoint = parseEndpoint(server, "2".equals(sslMode) ? 22 : 21);
|
||||||
|
return new TransferProfile(normalizedProfile, endpoint.host(), endpoint.port(), user, password, sslMode, normalizeDirectory(remotePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String loadProfileValue(String keyPrefix, String profile) {
|
||||||
|
return blankToEmpty(parameterService.getParameterValueWithFallback(keyPrefix + profile, 0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void uploadViaFtp(TransferProfile profile, String fileName, byte[] content) {
|
||||||
|
FTPClient client = new FTPClient();
|
||||||
|
try {
|
||||||
|
connectAndLogin(client, profile);
|
||||||
|
storeFile(client, profile.remotePath(), fileName, content);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("FTP-Upload fuer Profil " + profile.profileName() + " fehlgeschlagen.", exception);
|
||||||
|
} finally {
|
||||||
|
disconnectQuietly(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void uploadViaFtps(TransferProfile profile, String fileName, byte[] content) {
|
||||||
|
FTPSClient client = new FTPSClient(false);
|
||||||
|
try {
|
||||||
|
connectAndLogin(client, profile);
|
||||||
|
client.execPBSZ(0);
|
||||||
|
client.execPROT("P");
|
||||||
|
storeFile(client, profile.remotePath(), fileName, content);
|
||||||
|
} catch (IOException exception) {
|
||||||
|
throw new IllegalStateException("FTPS-Upload fuer Profil " + profile.profileName() + " fehlgeschlagen.", exception);
|
||||||
|
} finally {
|
||||||
|
disconnectQuietly(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectAndLogin(FTPClient client, TransferProfile profile) throws IOException {
|
||||||
|
client.connect(profile.host(), profile.port());
|
||||||
|
if (!client.login(profile.user(), profile.password())) {
|
||||||
|
throw new IOException("FTP-Login fehlgeschlagen.");
|
||||||
|
}
|
||||||
|
client.enterLocalPassiveMode();
|
||||||
|
client.setFileType(FTP.BINARY_FILE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeFile(FTPClient client, String remoteDirectory, String fileName, byte[] content) throws IOException {
|
||||||
|
ensureFtpDirectory(client, remoteDirectory);
|
||||||
|
try (InputStream inputStream = new ByteArrayInputStream(content)) {
|
||||||
|
if (!client.storeFile(fileName, inputStream)) {
|
||||||
|
throw new IOException("Datei konnte nicht gespeichert werden. Reply-Code: " + client.getReplyCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureFtpDirectory(FTPClient client, String remoteDirectory) throws IOException {
|
||||||
|
String[] segments = remoteDirectory.split("/");
|
||||||
|
if (remoteDirectory.startsWith("/")) {
|
||||||
|
client.changeWorkingDirectory("/");
|
||||||
|
}
|
||||||
|
for (String segment : segments) {
|
||||||
|
if (segment == null || segment.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!client.changeWorkingDirectory(segment)) {
|
||||||
|
if (!client.makeDirectory(segment) || !client.changeWorkingDirectory(segment)) {
|
||||||
|
throw new IOException("FTP-Verzeichnis konnte nicht angelegt werden: " + remoteDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disconnectQuietly(FTPClient client) {
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (client.isConnected()) {
|
||||||
|
client.logout();
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
} catch (IOException exception) {
|
||||||
|
log.debug("FTP disconnect failed: {}", exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void uploadViaSftp(TransferProfile profile, String fileName, byte[] content) {
|
||||||
|
Session session = null;
|
||||||
|
ChannelSftp channel = null;
|
||||||
|
try {
|
||||||
|
JSch jsch = new JSch();
|
||||||
|
session = jsch.getSession(profile.user(), profile.host(), profile.port());
|
||||||
|
session.setPassword(profile.password());
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.put("StrictHostKeyChecking", "no");
|
||||||
|
session.setConfig(properties);
|
||||||
|
session.connect();
|
||||||
|
|
||||||
|
channel = (ChannelSftp) session.openChannel("sftp");
|
||||||
|
channel.connect();
|
||||||
|
ensureSftpDirectory(channel, profile.remotePath());
|
||||||
|
channel.put(new ByteArrayInputStream(content), fileName);
|
||||||
|
} catch (JSchException | SftpException exception) {
|
||||||
|
throw new IllegalStateException("SFTP-Upload fuer Profil " + profile.profileName() + " fehlgeschlagen.", exception);
|
||||||
|
} finally {
|
||||||
|
if (channel != null) {
|
||||||
|
channel.disconnect();
|
||||||
|
}
|
||||||
|
if (session != null) {
|
||||||
|
session.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureSftpDirectory(ChannelSftp channel, String remoteDirectory) throws SftpException {
|
||||||
|
channel.cd("/");
|
||||||
|
for (String segment : remoteDirectory.split("/")) {
|
||||||
|
if (segment == null || segment.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
channel.cd(segment);
|
||||||
|
} catch (SftpException exception) {
|
||||||
|
channel.mkdir(segment);
|
||||||
|
channel.cd(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeDirectory(String value) {
|
||||||
|
String normalized = value != null ? value.trim() : "";
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
normalized = normalized.replace('\\', '/');
|
||||||
|
if (!normalized.startsWith("/")) {
|
||||||
|
normalized = "/" + normalized;
|
||||||
|
}
|
||||||
|
while (normalized.endsWith("/") && normalized.length() > 1) {
|
||||||
|
normalized = normalized.substring(0, normalized.length() - 1);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Endpoint parseEndpoint(String value, int defaultPort) {
|
||||||
|
String normalized = blankToEmpty(value);
|
||||||
|
int separator = normalized.lastIndexOf(':');
|
||||||
|
if (separator > 0 && separator < normalized.length() - 1 && normalized.indexOf(':') == separator) {
|
||||||
|
String host = normalized.substring(0, separator).trim();
|
||||||
|
String portValue = normalized.substring(separator + 1).trim();
|
||||||
|
if (portValue.matches("\\d+")) {
|
||||||
|
return new Endpoint(host, Integer.parseInt(portValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Endpoint(normalized, defaultPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToEmpty(String value) {
|
||||||
|
return value != null ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToDefault(String value, String fallback) {
|
||||||
|
String normalized = blankToEmpty(value);
|
||||||
|
return normalized.isBlank() ? fallback : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record TransferProfile(String profileName,
|
||||||
|
String host,
|
||||||
|
int port,
|
||||||
|
String user,
|
||||||
|
String password,
|
||||||
|
String sslMode,
|
||||||
|
String remotePath) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Endpoint(String host, int port) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package de.votian.services.service;
|
||||||
|
|
||||||
|
import de.votian.services.config.LegacySchedulingProperties;
|
||||||
|
import de.votian.services.security.AuthSessionService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class LegacyOperationsScheduler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LegacyOperationsScheduler.class);
|
||||||
|
|
||||||
|
private final LegacySchedulingProperties schedulingProperties;
|
||||||
|
private final AuthSessionService authSessionService;
|
||||||
|
private final LegacyCourierAutoLogoutService legacyCourierAutoLogoutService;
|
||||||
|
private final AcceptanceProtocolAutomationService acceptanceProtocolAutomationService;
|
||||||
|
|
||||||
|
public LegacyOperationsScheduler(LegacySchedulingProperties schedulingProperties,
|
||||||
|
AuthSessionService authSessionService,
|
||||||
|
LegacyCourierAutoLogoutService legacyCourierAutoLogoutService,
|
||||||
|
AcceptanceProtocolAutomationService acceptanceProtocolAutomationService) {
|
||||||
|
this.schedulingProperties = schedulingProperties;
|
||||||
|
this.authSessionService = authSessionService;
|
||||||
|
this.legacyCourierAutoLogoutService = legacyCourierAutoLogoutService;
|
||||||
|
this.acceptanceProtocolAutomationService = acceptanceProtocolAutomationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "${votian.scheduling.auth-session-cleanup-cron:0 */5 * * * *}",
|
||||||
|
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||||
|
public void cleanupExpiredAuthSessions() {
|
||||||
|
if (!schedulingProperties.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AuthSessionService.CleanupResult result = authSessionService.cleanupExpiredSessions();
|
||||||
|
if (result.totalExpiredSessions() > 0) {
|
||||||
|
log.info("Expired {} auth session(s) and {} pending TOTP session(s).",
|
||||||
|
result.expiredAuthenticatedSessions(),
|
||||||
|
result.expiredPendingTotpSessions());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "${votian.scheduling.courier-auto-logout-cron:0 */30 * * * *}",
|
||||||
|
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||||
|
public void runCourierAutoLogout() {
|
||||||
|
if (!schedulingProperties.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
legacyCourierAutoLogoutService.logoutCouriersByAutorevokes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "${votian.scheduling.acceptance-protocol-mail-cron:0 */5 * * * *}",
|
||||||
|
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||||
|
public void runAcceptanceProtocolMailDispatch() {
|
||||||
|
if (!schedulingProperties.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
acceptanceProtocolAutomationService.dispatchPendingMailProtocols();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "${votian.scheduling.acceptance-protocol-letter-cron:30 */5 * * * *}",
|
||||||
|
zone = "${votian.scheduling.zone:Europe/Berlin}")
|
||||||
|
public void runAcceptanceProtocolLetterDispatch() {
|
||||||
|
if (!schedulingProperties.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
acceptanceProtocolAutomationService.dispatchPendingLetterProtocols();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import de.votian.services.entity.Job;
|
|||||||
import de.votian.services.entity.ServiceZipCode;
|
import de.votian.services.entity.ServiceZipCode;
|
||||||
import de.votian.services.entity.Tour;
|
import de.votian.services.entity.Tour;
|
||||||
import de.votian.services.entity.VehicleDisposition;
|
import de.votian.services.entity.VehicleDisposition;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ public class LonghaulRemoteDbService {
|
|||||||
private final ParameterService parameterService;
|
private final ParameterService parameterService;
|
||||||
private final ConnectionFactory connectionFactory;
|
private final ConnectionFactory connectionFactory;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
public LonghaulRemoteDbService(ParameterService parameterService) {
|
public LonghaulRemoteDbService(ParameterService parameterService) {
|
||||||
this(parameterService, new DriverManagerConnectionFactory());
|
this(parameterService, new DriverManagerConnectionFactory());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,15 +113,11 @@ public class ViewDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<CustomerListItemDto> findCustomersByHeadquarters(Long hqId) {
|
public List<CustomerListItemDto> findCustomersByHeadquarters(Long hqId) {
|
||||||
return customerRepository.findByHeadquartersId(hqId).stream()
|
return toCustomerListItems(customerRepository.findByHeadquartersId(hqId));
|
||||||
.map(this::toCustomerListItem)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CustomerListItemDto> searchCustomers(Long hqId, String term) {
|
public List<CustomerListItemDto> searchCustomers(Long hqId, String term) {
|
||||||
return customerRepository.searchCustomers(hqId, term).stream()
|
return toCustomerListItems(customerRepository.searchCustomers(hqId, term));
|
||||||
.map(this::toCustomerListItem)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<CustomerDetailDto> findCustomerDetail(Long id) {
|
public Optional<CustomerDetailDto> findCustomerDetail(Long id) {
|
||||||
@@ -129,15 +125,11 @@ public class ViewDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<CourierListItemDto> findCouriersByHeadquarters(Long hqId) {
|
public List<CourierListItemDto> findCouriersByHeadquarters(Long hqId) {
|
||||||
return courierRepository.findByHeadquartersId(hqId).stream()
|
return toCourierListItems(courierRepository.findByHeadquartersId(hqId));
|
||||||
.map(this::toCourierListItem)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CourierListItemDto> searchCouriers(Long hqId, String term) {
|
public List<CourierListItemDto> searchCouriers(Long hqId, String term) {
|
||||||
return courierRepository.searchCouriers(hqId, term).stream()
|
return toCourierListItems(courierRepository.searchCouriers(hqId, term));
|
||||||
.map(this::toCourierListItem)
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<CourierDetailDto> findCourierDetail(Long id) {
|
public Optional<CourierDetailDto> findCourierDetail(Long id) {
|
||||||
@@ -475,10 +467,7 @@ public class ViewDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<CostCenter> findJobFilterCostCenters(Long hqId, List<Long> accessibleCostCenterIds) {
|
public List<CostCenter> findJobFilterCostCenters(Long hqId, List<Long> accessibleCostCenterIds) {
|
||||||
List<CostCenter> costCenters = new ArrayList<>();
|
List<CostCenter> costCenters = new ArrayList<>(costCenterRepository.findVisibleNonExternByHeadquartersId(hqId));
|
||||||
for (Customer customer : customerRepository.findByHeadquartersId(hqId)) {
|
|
||||||
costCenters.addAll(findVisibleCostCentersSorted(customer.getId()));
|
|
||||||
}
|
|
||||||
if (accessibleCostCenterIds != null) {
|
if (accessibleCostCenterIds != null) {
|
||||||
costCenters = filterCostCentersByAccess(costCenters, accessibleCostCenterIds);
|
costCenters = filterCostCentersByAccess(costCenters, accessibleCostCenterIds);
|
||||||
}
|
}
|
||||||
@@ -525,6 +514,51 @@ public class ViewDataService {
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<CustomerListItemDto> toCustomerListItems(List<Customer> customers) {
|
||||||
|
Map<Long, Company> companiesById = new LinkedHashMap<>();
|
||||||
|
for (Company company : companyRepository.findAllById(customers.stream()
|
||||||
|
.map(Customer::getCompanyId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||||
|
companiesById.put(company.getId(), company);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, Employee> employeesById = new LinkedHashMap<>();
|
||||||
|
for (Employee employee : employeeRepository.findAllById(customers.stream()
|
||||||
|
.map(Customer::getAdminEmployeeId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||||
|
employeesById.put(employee.getId(), employee);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, User> usersById = new LinkedHashMap<>();
|
||||||
|
for (User user : userRepository.findAllById(employeesById.values().stream()
|
||||||
|
.map(Employee::getUserId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||||
|
usersById.put(user.getId(), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return customers.stream()
|
||||||
|
.map(customer -> {
|
||||||
|
Company company = companiesById.get(customer.getCompanyId());
|
||||||
|
Employee employee = employeesById.get(customer.getAdminEmployeeId());
|
||||||
|
User user = employee != null ? usersById.get(employee.getUserId()) : null;
|
||||||
|
|
||||||
|
CustomerListItemDto dto = new CustomerListItemDto();
|
||||||
|
dto.setId(customer.getId());
|
||||||
|
dto.setEid(customer.getEid());
|
||||||
|
dto.setCompanyName(company != null && company.getComp() != null && !company.getComp().isBlank()
|
||||||
|
? company.getComp()
|
||||||
|
: customer.getEid());
|
||||||
|
dto.setContact(user != null ? joinName(user.getFirstname(), user.getName()) : "");
|
||||||
|
dto.setEmail(user != null && user.getEmail() != null ? user.getEmail() : "");
|
||||||
|
dto.setPhone(user != null && user.getPhone() != null ? user.getPhone() : "");
|
||||||
|
return dto;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
private CustomerDetailDto toCustomerDetail(Customer customer) {
|
private CustomerDetailDto toCustomerDetail(Customer customer) {
|
||||||
CustomerDetailDto dto = new CustomerDetailDto();
|
CustomerDetailDto dto = new CustomerDetailDto();
|
||||||
dto.setId(customer.getId());
|
dto.setId(customer.getId());
|
||||||
@@ -568,6 +602,42 @@ public class ViewDataService {
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<CourierListItemDto> toCourierListItems(List<Courier> couriers) {
|
||||||
|
Map<Long, Company> companiesById = new LinkedHashMap<>();
|
||||||
|
for (Company company : companyRepository.findAllById(couriers.stream()
|
||||||
|
.map(Courier::getCompanyId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||||
|
companiesById.put(company.getId(), company);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, User> usersById = new LinkedHashMap<>();
|
||||||
|
for (User user : userRepository.findAllById(couriers.stream()
|
||||||
|
.map(Courier::getUserId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)))) {
|
||||||
|
usersById.put(user.getId(), user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return couriers.stream()
|
||||||
|
.map(courier -> {
|
||||||
|
Company company = companiesById.get(courier.getCompanyId());
|
||||||
|
User user = usersById.get(courier.getUserId());
|
||||||
|
|
||||||
|
CourierListItemDto dto = new CourierListItemDto();
|
||||||
|
dto.setId(courier.getId());
|
||||||
|
dto.setEid(courier.getEid());
|
||||||
|
dto.setSid(courier.getSid());
|
||||||
|
dto.setCompanyName(company != null && company.getComp() != null && !company.getComp().isBlank()
|
||||||
|
? company.getComp()
|
||||||
|
: courier.getSid());
|
||||||
|
dto.setContact(user != null ? joinName(user.getFirstname(), user.getName()) : "");
|
||||||
|
dto.setAvailable("1".equals(courier.getAvailable()) ? "Ja" : "Nein");
|
||||||
|
return dto;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
private CourierDetailDto toCourierDetail(Courier courier) {
|
private CourierDetailDto toCourierDetail(Courier courier) {
|
||||||
CourierDetailDto dto = new CourierDetailDto();
|
CourierDetailDto dto = new CourierDetailDto();
|
||||||
dto.setId(courier.getId());
|
dto.setId(courier.getId());
|
||||||
|
|||||||
@@ -28,3 +28,15 @@ votian.storage.job-station-photos-dir=../html/temp/photos
|
|||||||
# Logging
|
# Logging
|
||||||
logging.level.de.votian=INFO
|
logging.level.de.votian=INFO
|
||||||
logging.level.org.hibernate.SQL=WARN
|
logging.level.org.hibernate.SQL=WARN
|
||||||
|
|
||||||
|
# Legacy scheduler bridge
|
||||||
|
votian.scheduling.enabled=true
|
||||||
|
votian.scheduling.zone=Europe/Berlin
|
||||||
|
votian.scheduling.auth-session-cleanup-cron=0 */5 * * * *
|
||||||
|
votian.scheduling.courier-auto-logout-cron=0 */30 * * * *
|
||||||
|
votian.scheduling.courier-auto-logout-headquarters-id=3
|
||||||
|
votian.scheduling.acceptance-protocol-mail-cron=0 */5 * * * *
|
||||||
|
votian.scheduling.acceptance-protocol-letter-cron=30 */5 * * * *
|
||||||
|
votian.scheduling.acceptance-protocol-mail-lookback-days=1
|
||||||
|
votian.scheduling.acceptance-protocol-letter-lookback-days=3
|
||||||
|
votian.scheduling.acceptance-protocol-letter-ftp-profile=MPS1
|
||||||
|
|||||||
@@ -63,8 +63,14 @@ public class DashboardView extends VerticalLayout {
|
|||||||
|
|
||||||
// Today's jobs count
|
// Today's jobs count
|
||||||
try {
|
try {
|
||||||
Object[] todayJobs = restClient.get("/jobs/today", Object[].class);
|
@SuppressWarnings("unchecked")
|
||||||
cards.add(createInfoCard("Heutige Auftraege", String.valueOf(todayJobs != null ? todayJobs.length : 0)));
|
Map<String, Object> result = restClient.get(
|
||||||
|
"/jobs/today/count",
|
||||||
|
LinkedHashMap.class
|
||||||
|
);
|
||||||
|
Object rawCount = result != null ? result.get("count") : null;
|
||||||
|
long count = rawCount instanceof Number ? ((Number) rawCount).longValue() : 0L;
|
||||||
|
cards.add(createInfoCard("Heutige Auftraege", String.valueOf(count)));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
cards.add(createInfoCard("Heutige Auftraege", "N/A"));
|
cards.add(createInfoCard("Heutige Auftraege", "N/A"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user