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`
|
||||
- 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`
|
||||
- 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
|
||||
|
||||
- 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
|
||||
|
||||
|
||||
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>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</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>
|
||||
<groupId>com.github.librepdf</groupId>
|
||||
<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());
|
||||
}
|
||||
|
||||
@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
|
||||
public ResponseEntity<Job> create(@RequestBody JobCreateRequest request, Authentication authentication) {
|
||||
UserSessionDto user = sessionUser(authentication);
|
||||
|
||||
@@ -69,9 +69,11 @@ public class Company {
|
||||
private String logo;
|
||||
|
||||
@Column(name = "cmp_logo_width")
|
||||
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||
private Integer logoWidth;
|
||||
|
||||
@Column(name = "cmp_logo_height")
|
||||
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||
private Integer logoHeight;
|
||||
|
||||
@Column(name = "cmp_modify_status")
|
||||
|
||||
@@ -23,18 +23,12 @@ public class CostCenter {
|
||||
@Column(name = "csc_path")
|
||||
private String path;
|
||||
|
||||
@Column(name = "csc_id_payer")
|
||||
private Long payerId;
|
||||
|
||||
@Column(name = "csc_visible")
|
||||
private String visible;
|
||||
|
||||
@Column(name = "csc_is_extern")
|
||||
private String isExtern;
|
||||
|
||||
@Column(name = "emp_id_related")
|
||||
private Long relatedEmployeeId;
|
||||
|
||||
public CostCenter() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
@@ -47,12 +41,8 @@ public class CostCenter {
|
||||
public void setParentId(Long parentId) { this.parentId = parentId; }
|
||||
public String getPath() { return 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 void setVisible(String visible) { this.visible = visible; }
|
||||
public String getIsExtern() { return 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;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.Formula;
|
||||
|
||||
@Entity
|
||||
@Table(name = "employee")
|
||||
@@ -14,7 +15,7 @@ public class Employee {
|
||||
@Column(name = "usr_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(name = "hq_id")
|
||||
@Formula("(select u.hq_id from user u where u.usr_id = usr_id)")
|
||||
private Long headquartersId;
|
||||
|
||||
@Column(name = "csc_id")
|
||||
|
||||
@@ -10,7 +10,7 @@ import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "appointment", schema = "phoenix_group")
|
||||
@Table(name = "appointment", catalog = "phoenix_group")
|
||||
public class GroupwareAppointment {
|
||||
|
||||
@Id
|
||||
|
||||
@@ -149,6 +149,7 @@ public class Job {
|
||||
private Integer longhaul;
|
||||
|
||||
@Column(name = "jb_service")
|
||||
@Convert(converter = LegacyBlankableIntegerStringConverter.class)
|
||||
private Integer service;
|
||||
|
||||
@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'")
|
||||
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")
|
||||
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")
|
||||
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 " +
|
||||
"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);
|
||||
|
||||
@@ -36,10 +36,11 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
List<User> findByTypeAndHeadquartersIdOrderByNameAscFirstnameAsc(@Param("type") Integer type,
|
||||
@Param("hqId") Long headquartersId);
|
||||
|
||||
@Query("SELECT DISTINCT hq.mnemonic, u.name, u.firstname, u.type " +
|
||||
"FROM User u JOIN Headquarters hq ON u.headquartersId = hq.id " +
|
||||
"WHERE FUNCTION('RIGHT', u.birthdate, 5) = FUNCTION('RIGHT', CURRENT_DATE, 5) " +
|
||||
"ORDER BY u.type, u.name")
|
||||
@Query(value = "SELECT DISTINCT hq.hq_mnemonic, u.usr_name, u.usr_firstname, u.usr_type " +
|
||||
"FROM user u JOIN headquarters hq ON u.hq_id = hq.hq_id " +
|
||||
"WHERE DATE_FORMAT(u.usr_birthdate, '%m-%d') = DATE_FORMAT(CURRENT_DATE, '%m-%d') " +
|
||||
"ORDER BY u.usr_type, u.usr_name",
|
||||
nativeQuery = true)
|
||||
List<Object[]> findBirthdaysToday();
|
||||
|
||||
@Modifying
|
||||
|
||||
@@ -57,6 +57,33 @@ public class AuthSessionService {
|
||||
return Optional.of(state.user());
|
||||
}
|
||||
|
||||
private record SessionState(UserSessionDto user, Instant expiresAt) { }
|
||||
private record PendingTotpState(UserSessionDto user, Instant expiresAt) { }
|
||||
public CleanupResult cleanupExpiredSessions() {
|
||||
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.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@@ -90,6 +91,15 @@ public class JobService {
|
||||
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
|
||||
public Job createJob(Job job, List<Tour> tours, List<TourArticle> articles) {
|
||||
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.Tour;
|
||||
import de.votian.services.entity.VehicleDisposition;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -39,6 +40,7 @@ public class LonghaulRemoteDbService {
|
||||
private final ParameterService parameterService;
|
||||
private final ConnectionFactory connectionFactory;
|
||||
|
||||
@Autowired
|
||||
public LonghaulRemoteDbService(ParameterService parameterService) {
|
||||
this(parameterService, new DriverManagerConnectionFactory());
|
||||
}
|
||||
|
||||
@@ -113,15 +113,11 @@ public class ViewDataService {
|
||||
}
|
||||
|
||||
public List<CustomerListItemDto> findCustomersByHeadquarters(Long hqId) {
|
||||
return customerRepository.findByHeadquartersId(hqId).stream()
|
||||
.map(this::toCustomerListItem)
|
||||
.toList();
|
||||
return toCustomerListItems(customerRepository.findByHeadquartersId(hqId));
|
||||
}
|
||||
|
||||
public List<CustomerListItemDto> searchCustomers(Long hqId, String term) {
|
||||
return customerRepository.searchCustomers(hqId, term).stream()
|
||||
.map(this::toCustomerListItem)
|
||||
.toList();
|
||||
return toCustomerListItems(customerRepository.searchCustomers(hqId, term));
|
||||
}
|
||||
|
||||
public Optional<CustomerDetailDto> findCustomerDetail(Long id) {
|
||||
@@ -129,15 +125,11 @@ public class ViewDataService {
|
||||
}
|
||||
|
||||
public List<CourierListItemDto> findCouriersByHeadquarters(Long hqId) {
|
||||
return courierRepository.findByHeadquartersId(hqId).stream()
|
||||
.map(this::toCourierListItem)
|
||||
.toList();
|
||||
return toCourierListItems(courierRepository.findByHeadquartersId(hqId));
|
||||
}
|
||||
|
||||
public List<CourierListItemDto> searchCouriers(Long hqId, String term) {
|
||||
return courierRepository.searchCouriers(hqId, term).stream()
|
||||
.map(this::toCourierListItem)
|
||||
.toList();
|
||||
return toCourierListItems(courierRepository.searchCouriers(hqId, term));
|
||||
}
|
||||
|
||||
public Optional<CourierDetailDto> findCourierDetail(Long id) {
|
||||
@@ -475,10 +467,7 @@ public class ViewDataService {
|
||||
}
|
||||
|
||||
public List<CostCenter> findJobFilterCostCenters(Long hqId, List<Long> accessibleCostCenterIds) {
|
||||
List<CostCenter> costCenters = new ArrayList<>();
|
||||
for (Customer customer : customerRepository.findByHeadquartersId(hqId)) {
|
||||
costCenters.addAll(findVisibleCostCentersSorted(customer.getId()));
|
||||
}
|
||||
List<CostCenter> costCenters = new ArrayList<>(costCenterRepository.findVisibleNonExternByHeadquartersId(hqId));
|
||||
if (accessibleCostCenterIds != null) {
|
||||
costCenters = filterCostCentersByAccess(costCenters, accessibleCostCenterIds);
|
||||
}
|
||||
@@ -525,6 +514,51 @@ public class ViewDataService {
|
||||
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) {
|
||||
CustomerDetailDto dto = new CustomerDetailDto();
|
||||
dto.setId(customer.getId());
|
||||
@@ -568,6 +602,42 @@ public class ViewDataService {
|
||||
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) {
|
||||
CourierDetailDto dto = new CourierDetailDto();
|
||||
dto.setId(courier.getId());
|
||||
|
||||
@@ -28,3 +28,15 @@ votian.storage.job-station-photos-dir=../html/temp/photos
|
||||
# Logging
|
||||
logging.level.de.votian=INFO
|
||||
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
|
||||
try {
|
||||
Object[] todayJobs = restClient.get("/jobs/today", Object[].class);
|
||||
cards.add(createInfoCard("Heutige Auftraege", String.valueOf(todayJobs != null ? todayJobs.length : 0)));
|
||||
@SuppressWarnings("unchecked")
|
||||
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) {
|
||||
cards.add(createInfoCard("Heutige Auftraege", "N/A"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user