From 9ff4e95e6fe1e305c32a5b7adfb4986938fa26e5 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Thu, 2 Apr 2026 12:47:14 +0200 Subject: [PATCH] Document legacy workflows and stabilize migrated services --- .claude/settings.local.json | 7 + MIGRATION_BACKLOG.md | 4 +- html/DOKUMENTATION.md | 1215 +++++++++++++++++ html/DOKUMENTATION_NIEDERLASSUNGEN.md | 445 ------ services/pom.xml | 10 + .../config/LegacySchedulingProperties.java | 98 ++ .../services/config/SchedulingConfig.java | 11 + .../services/controller/JobController.java | 26 + .../de/votian/services/entity/Company.java | 2 + .../de/votian/services/entity/CostCenter.java | 10 - .../de/votian/services/entity/Employee.java | 3 +- .../services/entity/GroupwareAppointment.java | 2 +- .../java/de/votian/services/entity/Job.java | 1 + ...LegacyBlankableIntegerStringConverter.java | 25 + .../repository/CostCenterRepository.java | 9 + .../services/repository/JobRepository.java | 5 + .../services/repository/UserRepository.java | 9 +- .../services/security/AuthSessionService.java | 31 +- .../AcceptanceProtocolAutomationService.java | 453 ++++++ .../votian/services/service/JobService.java | 10 + .../LegacyCourierAutoLogoutService.java | 171 +++ .../service/LegacyFileTransferService.java | 231 ++++ .../service/LegacyOperationsScheduler.java | 70 + .../service/LonghaulRemoteDbService.java | 2 + .../services/service/ViewDataService.java | 102 +- .../src/main/resources/application.properties | 12 + .../de/votian/web/view/DashboardView.java | 10 +- 27 files changed, 2492 insertions(+), 482 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 html/DOKUMENTATION.md delete mode 100644 html/DOKUMENTATION_NIEDERLASSUNGEN.md create mode 100644 services/src/main/java/de/votian/services/config/LegacySchedulingProperties.java create mode 100644 services/src/main/java/de/votian/services/config/SchedulingConfig.java create mode 100644 services/src/main/java/de/votian/services/entity/LegacyBlankableIntegerStringConverter.java create mode 100644 services/src/main/java/de/votian/services/service/AcceptanceProtocolAutomationService.java create mode 100644 services/src/main/java/de/votian/services/service/LegacyCourierAutoLogoutService.java create mode 100644 services/src/main/java/de/votian/services/service/LegacyFileTransferService.java create mode 100644 services/src/main/java/de/votian/services/service/LegacyOperationsScheduler.java diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..65c8766 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(bash:*)" + ] + } +} diff --git a/MIGRATION_BACKLOG.md b/MIGRATION_BACKLOG.md index a545411..ff13bfb 100644 --- a/MIGRATION_BACKLOG.md +++ b/MIGRATION_BACKLOG.md @@ -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 diff --git a/html/DOKUMENTATION.md b/html/DOKUMENTATION.md new file mode 100644 index 0000000..84cf973 --- /dev/null +++ b/html/DOKUMENTATION.md @@ -0,0 +1,1215 @@ +# Dokumentation: Fachliche Prozesse der Legacy-PHP-Anwendung + +Stand: 2026-03-31 + +## 1. Ziel und Leselogik + +Dieses Dokument beschreibt die produktiv erreichbaren fachlichen Prozesse der Legacy-PHP-Anwendung unter `html/`. + +Der Aufbau ist bewusst fuer fachliche Leser optimiert. Jeder fachliche Vertikalschnitt folgt derselben Reihenfolge: + +1. Was passiert fachlich? +2. Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? +3. Welche Eingabefelder sind zu fuellen? +4. Woher kommen die Daten? +5. Was passiert mit den Daten? +6. Wohin werden die Daten geschrieben? +7. Wodurch wird der Ablauf gesteuert? + +Die Beschreibung basiert auf den real erreichbaren Legacy-Pfaden und den zentralen Includes, insbesondere: + +- `index.php`, `admin/start.php`, `admin/menu.php` +- `include/auth.inc.php`, `include/dbglobal.inc.php`, `include/mcglobal.inc.php`, `include/caglobal.inc.php` +- `html/index_php_reachable_paths.txt`, `html/index_php_call_graph.txt` +- `MIGRATION_BACKLOG.md`, `LEGACY_SIDEPATH_AUDIT.md` + +Nicht im Fokus stehen rein technische Drittbibliotheken unter `lib/`, `PEAR_XXXX/`, `js/`, `css/` sowie bewusst archivierte Altpfade ohne regulaeren Fachzugriff. + +--- + +## 2. Systemrahmen + +### 2.1 Einstieg und organisatorischer Kontext + +Die Anwendung startet ueber `index.php` und leitet auf `admin/start.php` weiter. Fachlich ist das System ein Multi-Tenant-Portal mit gemeinsamem Code, aber HQ-bezogener Daten- und Parametersteuerung. + +Zentrale fachliche Scope-Felder: + +- `md_id`: Mandant +- `hq_id`: Niederlassung / Standort +- `emp_id`: Mitarbeiterkontext +- `usr_type`: Benutzertyp + +Benutzertypen: + +| `usr_type` | Rolle | Hauptprozesse | +|---|---|---| +| `1` | HQ-Benutzer | Verwaltung, Auftraege, Listen, Rechnungen, Vertrieb, Nachrichten, Datentransfer, Statistik, Formulare | +| `2` | Kundenbenutzer | Kostenstellen, Auftragserfassung, Auftragslisten, Datenexport, Statistik, Rechnungen | +| `3` | Kurierbenutzer | Eigene Auftraege, Passwort, mobile Kommunikation | +| `4` | Lagerbenutzer | Lagerverwaltung | + +### 2.2 Parameter- und Konfigurationslogik + +Die zentrale Steuerung liegt in der Tabelle `parameter` und in den Funktionen aus `include/dbglobal.inc.php`. + +Wesentliche Zugriffsfunktionen: + +- `getParameterArray($empId, $hqId)` +- `defineGlobalParameters($hqId)` +- `getParameterValue($empId, $key, $hqId)` +- `getObjectBasedParameterValue($key, $objId, ...)` +- `setParameterValue(...)` + +Typische Parameterfamilien: + +- `MASK_*`: Masken-, Anzeige- und Prozesssteuerung +- `CUSTOMER_*`, `CS_*`: Kundenlogik +- `CR_*`, `CRVH_*`: Kurier- und Fahrzeuglogik +- `RANKING_*`, `AUTORANKING_*`, `LONGHAUL_*`: Disposition +- `EXPORT_*`, `FTP_*`, `DATATRANSFER_*`: Export und Datentransfer +- `MAIL_*`, `AUTOMAILER_*`: Mail- und Versandlogik +- `LOCATING_*`, `GEO_*`, `PZM_*`: Geocoding, Ortung und Routing + +Viele Regeln liegen nicht als feste Tabellenfelder, sondern als dynamische Parameterschluessel vor, zum Beispiel: + +- `CUSTOMER_MASK_JOBLIST_CANCELLATION_ENABLED_` +- `ORDER_REQUEST_VHT_ID_DEFAULT_` +- `TRACKING_ENABLED_CS_` +- `EXPORT_SPECIAL_FUNCTIONNAME_SUFFICES_CS_` + +### 2.3 Zentrale Datenbereiche + +| Bereich | Typische Tabellen / Schemas | +|---|---| +| Benutzer und Organisation | `user`, `employee`, `headquarters`, `mandatorheadquarters`, `company`, `rights`, `employeerights` | +| Kunden und Kostenstellen | `customer`, `costcenter`, `costcenteraddress`, `customercourier` | +| Transporteure und Fahrzeuge | `courier`, `couriervehicle`, `messagegroup` | +| Auftraege | `job`, `tour`, `tourservice`, `jobprice`, `invoice`, `jobbatch`, `jobbatchlist` | +| Preise und Services | `service`, `servicetype`, `servicehistory`, `servicecustomer`, `serviceplz*`, `serviceplzarea*`, `serviceradius`, `servicezone*` | +| Kommunikation und Vertrieb | `phoenix_group.appointment`, `phoenix_group.report_process`, `phoenix_group.tickerforum`, `phoenix_log.messageforum` | +| Logging und technische Steuerung | `phoenix_log.log`, `phoenix_log.semaphor`, `phoenix_log.locating`, `phoenix_log.route`, `phoenix_pda.commandexec` | +| Flexible Zusatzdaten | `genericdatacontainer`, `metafield*`, `metatype` | +| Geo- und Adressdaten | `address`, `phoenix_special.street`, `address_geo.address_geo`, `address_geo.distance`, `serviceplztraveltime`, `publicholiday` | +| Lager | `stock`, `stockarticle`, `article`, `articlegroup*`, `articleitem`, `stockmove` | +| Externe Identitaeten | `meta_object.metaobject` | + +### 2.4 Dateibasierte Speicherorte + +Neben der Datenbank nutzt die Anwendung mehrere fachlich relevante Verzeichnisse: + +- `html/import/upload/`: Uploads und Importstaging +- `html/export/download/`: erzeugte Exporte +- `html/temp/download/`: temporaere Downloads +- `html/temp/pdf/`: PDF-Zwischenablage +- `html/temp/photos/`: Stations- und POD-Fotos +- `html/temp/signs/`: Signaturen +- `html/images/external/`: Logos und externe Bilder +- `html/log/`: XML-, Prozess-, Export- und Mail-Logs + +--- + +## 3. Fachliche Vertikalschnitte + +### 3.1 Authentifizierung, Session und Startseite + +#### Was passiert? + +Benutzer melden sich an, werden einem HQ- und Mitarbeiterkontext zugeordnet, durchlaufen bei Bedarf 2FA und landen anschliessend auf einer rollenabhaengigen Startseite. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- gueltige Zugangsdaten eingeben +- den richtigen Benutzer- und HQ-Kontext verwenden +- bei aktivierter 2FA den Code bestaetigen +- einen geforderten Passwortwechsel oder Reset vollstaendig abschliessen + +#### Welche Eingabefelder sind zu fuellen? + +- Benutzerkonto oder Anmeldename (`f_chk_account`) +- Passwort (`f_chk_password`) +- bei SSO-Faellen die Kontoauswahl (`sso_selected_account`) +- bei aktivierter Zusatzsicherung 2FA-Code oder Geraetebestaetigung + +#### Woher kommen die Daten? + +- aus `user`, `employee`, `headquarters`, `mandatorheadquarters` +- aus `parameter` fuer Login- und Spracheinstellungen +- aus `genericdatacontainer` fuer Passwortwechselpflicht +- aus den HTTP-Parametern und der PHP-Session + +#### Was passiert mit den Daten? + +- Zugangsdaten werden geprueft. +- HQ, `emp_id`, `usr_type` und Mandant werden aufgeloest. +- Sessiondaten werden aufgebaut oder verworfen. +- TOTP-Status wird geprueft. +- Bei Passwort-Reset wird ein Einmalpasswort erzeugt und per Mail versendet. +- Die Startseite liest Geburtstage, offene Mitteilungen, Reporthinweise und Suchdaten nur zur Anzeige aus. + +#### Wohin werden die Daten geschrieben? + +- in die PHP-Session +- bei Passwort-Reset nach `user.usr_password` und `user.usr_password_modify` +- indirekt in Mailversand und Mail-Logs + +#### Wodurch wird der Ablauf gesteuert? + +- `LOGIN_FORGOT_PASSWORD_ENABLED` +- `MAXIMUM_LOGIN_TRIALS` +- `SYSTEM_LANGUAGE_DEFAULT` +- `HEADQUARTERS_MULTIPLE_ACCESS_EMPLOYEES` +- `HTTP_VARS_SEC_STATE` +- `HTTP_VARS_SEC_SEQ` + +### 3.2 HQ-, Mitarbeiter- und Rechteverwaltung + +#### Was passiert? + +HQ-Benutzer, Kundenbenutzer und Lagernutzer werden angelegt, gepflegt und mit Rechten, HQ-Freigaben, Kostenstellenrechten, Lagerrechten und individuellen Maskeneinstellungen versehen. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- Benutzerkonto und Mitarbeiterstammdaten vollstaendig erfassen oder oeffnen +- den richtigen Benutzertyp festlegen +- Rechte, HQ-Freigaben, Kostenstellen- und Lagerzugaenge passend setzen +- Aenderungen speichern und bei Bedarf Passwort oder 2FA-Status initialisieren + +#### Welche Eingabefelder sind zu fuellen? + +- HQ-, Mitarbeiter- und Benutzerkontext (`f_hq_id`, `emp_id_act`, Benutzertyp) +- Name, Vorname, E-Mail, Telefon, Zusatztelefon und Geburtsdatum (`usr_name`, `usr_firstname`, `usr_email`, `usr_phone`, `usr_phone2`, `f_usr_birthdate_*`) +- Benutzerkonto, Passwort und Passwortwiederholung (`usr_account`, `usr_password`, `usr_password2`) +- Rechte-, HQ-, Kostenstellen-, Lager- und Listenparameter (`emp_rights`, `par_*`, `par_stock_access`) + +#### Woher kommen die Daten? + +- aus `user`, `employee`, `headquarters`, `rights`, `employeerights`, `costcenter` +- aus `parameter` fuer Layout-, Listen- und Lagerkonfiguration +- aus `meta_object.metaobject` fuer externe Objektkopplungen + +#### Was passiert mit den Daten? + +- Benutzerkonten und Mitarbeiterdatensaetze werden erstellt oder geaendert. +- Passwortaenderungen werden ausgefuehrt. +- HQ-Freigaben und Rechtebits werden gesetzt. +- Lagerzugriffe und Kostenstellenzugriffe werden auf Mitarbeiterebene verdrahtet. +- Mitarbeiterbezogene Listen- und Maskenparameter werden gesetzt. + +#### Wohin werden die Daten geschrieben? + +- nach `user` +- nach `employee` +- nach `employeerights` +- nach `parameter` +- nach `meta_object.metaobject` + +#### Wodurch wird der Ablauf gesteuert? + +- `EMP_BITSTR_MAXLEN` +- `MASK_EMP_CSC_MATRIX_ENABLED` +- `GLOBAL_CUSTOMER_READONLY_DISABLED` +- `USERTYPE_2FA_ENABLED` +- `USER_2FA_NO_DEACTIVATION` +- `HEADQUARTERS_MULTIPLE_ACCESS_EMPLOYEES` +- `MASK_JOBLIST_*` +- `MASK_CS_LIST_*` +- `MASK_CR_LIST_*` +- `MASK_STK_*` +- `LOCATING_PDA_ENABLED` +- `LOCATING_PDA_INTERVAL` + +### 3.3 Kundenstammdaten, Kostenstellen und Kundenspezifika + +#### Was passiert? + +Kundenfirmen, Kunden, Root- und Unterkostenstellen, Rechnungs- und Lieferadressen sowie kundenspezifische Verhaltensregeln werden gepflegt. Ausserdem werden bevorzugte Kuriere und Servicezuordnungen hinterlegt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- Firma, Kunde und Kostenstelle im richtigen organisatorischen Kontext anlegen oder auswaehlen +- Pflichtdaten wie Namen, Kennungen, Kontakt- und Adressdaten vollstaendig pflegen +- Rechnungs-, Liefer- und Abrechnungsbezug korrekt zuordnen +- kundenspezifische Regeln, Favoritenkuriere und Services speichern + +#### Welche Eingabefelder sind zu fuellen? + +- Firmenname, Zusatznamen und Identifikatoren (`f_cmp_comp`, `f_cmp_comp2`, `f_cmp_comp3`, `f_cmp_comp4`, `f_cs_eid_old`) +- Ansprechpartner-, Login- und Kommunikationsfelder (`f_usr_name`, `f_usr_firstname`, `f_usr_email`, `f_usr_phone`, `f_usr_phone2`, `f_usr_fax`, `f_usr_inv_email`, `f_usr_reminder_email`, `f_usr_account`) +- Steuer-, Bank- und Zahlungsfelder (`f_cmp_tax_idno`, `f_cmp_stax_idno`, `f_cmp_bank*`, `f_cmp_iban`, `f_cmp_swift`, `f_cmp_bankmode`) +- Adressfelder fuer Haupt-, Rechnungs-, Leistungs- und Abhol-/Lieferadressen (`f_ad_street`, `f_cmp_hsno`, `f_ad_zipcode`, `f_ad_city`, `f_ad_country`, `f_cscad_*`) +- Kundenoptionen wie Tracking, Statusmail, Foto, Newsletter, DSGVO, POD und Zeitsteuerung (`f_cs_tracking[]`, `f_cs_jbstatusmail*`, `f_cs_photo_*`, `f_cmp_newsletter`, `f_cmp_dsgvo`, `f_cmp_proof_delivery`, `f_cs_charging_time`) + +#### Woher kommen die Daten? + +- aus `company`, `customer`, `costcenter`, `costcenteraddress`, `address` +- aus `customercourier` +- aus `service`, `servicetype`, `customerservice`, `customerservicetype` +- aus `serviceradius`, `servicezone`, `servicezonemapping` +- aus `genericdatacontainer` +- aus `parameter` + +#### Was passiert mit den Daten? + +- Firmen- und Kundendaten werden validiert. +- Root- und Unterkostenstellen werden erzeugt oder geaendert. +- Rechnungs- und Lieferadressen werden auf bestehende Adressen gemappt oder neu angelegt. +- Kundenspezifische Regeln fuer Joblisten, Tracking, Mailsprache, Standardpreisunterdrueckung und Self-Service-Verhalten werden gepflegt. +- Favoritenkuriere und Services werden dem Kunden zugeordnet. + +#### Wohin werden die Daten geschrieben? + +- nach `user` +- nach `employee` +- nach `company` +- nach `customer` +- nach `costcenter` +- nach `costcenteraddress` +- nach `customercourier` +- nach `customerservice` +- nach `customerservicetype` +- nach `genericdatacontainer` +- bei externer Kopplung nach `meta_object.metaobject` + +#### Wodurch wird der Ablauf gesteuert? + +- `CS_EID_PREFIX` +- `CS_PHONE_MINIMUM_LENGTH` +- `CS_TRACKING_ENABLED` +- `MASK_CS_SPECIAL_TAX_IDS_CHANGE_MSG` +- `CS_JB_JAM_WAITTIME_*` +- `MASK_CS_NEW_SIMILARITY_SEARCH` +- `MASK_CS_CHECK_INTERNAL_BOOKING_ACCOUNT` +- `MASK_CS_PROV_RANGE_*` +- `MASK_CS_SHOW_LAST_JOB` +- `CUSTOMER_MASK_JOBLIST_*` +- `CUSTOMER_MASK_CALCULATOR_USAGE_ONLY_` +- `MASK_CS_DONT_MAKE_STANDARD_PRICE_` +- `JOBDETAILS_EMAIL_LANGUAGE_` +- `JOB_MAIL_STATION_ARRIVAL_TIME_` + +### 3.4 Kundenservice-Konfiguration: Delivery Reasons und Acceptance Protocols + +#### Was passiert? + +Pro Kunde und Service werden mobile Abliefergruende, Codes, Mailregeln, Fragen fuer Abnahmeprotokolle und Textbausteine gepflegt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- den richtigen Kunden und den betroffenen Service auswaehlen +- Abliefergruende, Codes und Textbausteine fachlich vollstaendig pflegen +- Fragen und Mailstatus fuer das Abnahmeprotokoll definieren +- die Konfiguration speichern und je Service pruefen + +#### Welche Eingabefelder sind zu fuellen? + +- Kunden- und Serviceauswahl +- Abliefercode und Abliefergrund je Zeile (`f_code_XX`, `f_reason_XX`) +- Mailkennzeichen je Grund oder Frage (`f_mail_XX`) +- Fragen des Abnahmeprotokolls (`f_question_XX`) +- Freitextbausteine fuer Delivery Reasons und Acceptance Protocols + +#### Woher kommen die Daten? + +- aus `genericdatacontainer` +- aus `metatype` fuer die Serviceauswahl +- aus Formulardaten der Admin-Masken + +#### Was passiert mit den Daten? + +- Abliefergruende werden je Liste oder Service gesammelt. +- Fragen und Mailstatus fuer Abnahmeprotokolle werden je Service verdichtet. +- Texte werden nicht als einzelne Tabellenfelder, sondern als serialisierte Objektzusaetze abgelegt. + +#### Wohin werden die Daten geschrieben? + +- nach `genericdatacontainer` + +Verwendete `gdc_gen_fieldname`-Werte: + +- `service_delivery_reasons` +- `service_delivery_reasons_mail_state` +- `service_delivery_reasons_text` +- `service_acceptance_protocol_questions` +- `service_acceptance_protocol_mail_state_question` +- `service_acceptance_protocol_text` + +#### Wodurch wird der Ablauf gesteuert? + +- `CUSTOMER_SERVICE_DELIVERY_REASONS` +- die allgemeine Kundenservice-Konfiguration des Kunden + +### 3.5 Transporteur-, Fahrer- und Fahrzeugstammdaten + +#### Was passiert? + +Transporteur-Firmen, Fahrerkonten, Fahrzeugstammdaten, mobile Geraetemerkmale, Nachrichtengruppen und Kundenzuordnungen werden gepflegt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- Transporteur, Fahrer oder Fahrzeug im richtigen Pflegebereich auswaehlen oder neu anlegen +- Kennungen, Accountdaten, Fahrzeugmerkmale und mobile Eigenschaften vollstaendig erfassen +- Kundenzuordnungen und Nachrichtengruppen fachlich passend hinterlegen +- die Stammdaten speichern und bei Bedarf Dokumentbezug oder Lagerbezug ergaenzen + +#### Welche Eingabefelder sind zu fuellen? + +- Transporteurstammdaten wie Firmenname, EID/SID, Vertragsdatum und Bemerkungen (`f_cmp_comp*`, `f_cr_eid`, `f_cr_sid`, `f_cmp_contract_date_*`, `f_cmp_remark*`) +- Fahrer- und Loginfelder (`f_usr_name`, `f_usr_firstname`, `f_usr_country`, `f_usr_phone*`, `f_usr_email`, `f_usr_inv_email`, `f_usr_account`, `f_usr_password*`, `f_usr_birthdate_*`) +- Adress-, Bank- und Steuerfelder (`f_ad_*`, `f_cmp_hsno`, `f_cmp_bank*`, `f_cmp_iban`, `f_cmp_swift`, `f_cmp_tax_idno`, `f_cmp_stax_idno`) +- Mobile und Verfuegbarkeitsfelder (`f_cr_imei`, `f_cr_serialno`, `f_cr_mobile_pda`, `f_cr_disposition[]`, `f_cr_price_show[]`, `f_cr_app_blocked[]`, `f_cmp_no_longhaul[]`) +- Fahrzeug- und Beziehungsfelder wie Typ, SID, Kennzeichen, Masse, Versicherungen, Partnerkonditionen und Kundenrelation (`f_vht_id`, `f_crvh_sid`, `f_crvh_vh_sign`, `f_crvh_payload`, `f_crvh_totalweight`, `f_crvh_length`, `f_crvh_width`, `f_crvh_height`, `f_crvh_*insurance*`, `f_crvh_partner_*`, `f_cscr_description_new`, `f_relationStatus`, `f_msggrp`) + +#### Woher kommen die Daten? + +- aus `company`, `courier`, `couriervehicle`, `user`, `address` +- aus `customercourier` +- aus `messagegroup` +- aus `stock` +- aus `genericdatacontainer` +- aus `parameter` + +#### Was passiert mit den Daten? + +- neue Fahrer und Transporteurkonten werden erzeugt +- EID, SID und Accountdaten werden validiert +- Fahrzeuge werden mit Typ, Groessen, Provisionen und Dokumentbezug gepflegt +- Nachrichtengruppen werden einem Kurier zugeordnet +- Kunden und Transporteure werden als Favoritenbeziehung verdrahtet + +#### Wohin werden die Daten geschrieben? + +- nach `user` +- nach `company` +- nach `courier` +- nach `couriervehicle` +- nach `customercourier` +- nach `courier.cr_msggrp` +- indirekt in Uploadverzeichnisse fuer Fahrzeugdokumente + +#### Wodurch wird der Ablauf gesteuert? + +- `CR_EID_PREFIX` +- `CR_SID_PREFIX` +- `MASK_COURIER_EID_LENGTH` +- `MASK_COURIER_EID_MAX_LENGTH` +- `MASK_CR_MOBILE_PDA_EDIT` +- `MASK_HIDE_CR_VHT_INV` +- `MASK_CR_CARTAGE_LINKS_DISABLED` +- `GLOBAL_VEHICLE_COURIER_RELATION` +- `GLOBAL_VEHICLE_STOCK_RELATION` +- `VEHICLE_STOCK_PARENT_ID` +- `MASK_CRVH_TOTALWEIGHT_MANDATORY` +- `MASK_CRVH_PARTNER_PROV_RANGE_*` +- `DATATRANSFER_DIRECTORY_CRVH` +- `DPF_CR_ENABLED` +- `MASK_EXCLUDE_VHT_IDS` + +### 3.6 Service-, Preis-, Rabatt- und Gebietsverwaltung + +#### Was passiert? + +Standardpreise, kundenbezogene Preise, Kurierpreise, Rabatte, PLZ-Matrizen, Bereichsmatrizen und Nachbargebiete werden gepflegt. Diese Stammdaten sind die Basis fuer die operative Preisfindung im Auftrag. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- den richtigen Service, Kundenbezug oder Gebietsbezug auswaehlen +- Preis-, Rabatt- oder Gebietszeilen mit allen Pflichtwerten erfassen +- Gueltigkeiten, Historie und Fahrzeugtypbezug korrekt pflegen +- die Pflege speichern und die betroffene Preislogik fachlich gegenpruefen + +#### Welche Eingabefelder sind zu fuellen? + +- Gueltigkeitsdatum und Arbeitsmodus (`day_from`, `month_from`, `year_from`, Preis-/Rabattmodus) +- Service- und Servicetyppflege (`f_service_id`, `f_service_new`, `f_servicetype_id`, `f_servicetype_new`) +- Preis- und Rabattmatrix je Service/Servicetyp (`service__`, `service_cr__`) +- Kundenservices, Zeitfenster und Rabattmodi (`f_cs_service[]`, `f_cs_servicetype[]`, `f_day_time_*`, `f_delivery_time_*`, `f_sms_time_*`, `f_cartage_time_*`, `f_discount_mode_*`) +- Radius-, PLZ- und Zonenfelder (`f_radius_zipcode_new`, `f_radius_district_new`, `f_radius_zone_new`, `f_zone_name_new`, `f_zone_zipcode_new`, `f_zone_zone_new`) + +#### Woher kommen die Daten? + +- aus `service`, `servicetype` +- aus `servicehistory`, `servicecustomer` +- aus `serviceplz`, `serviceplzhistory`, `serviceplzcustomer`, `serviceplzneighbour` +- aus `serviceplzarea`, `serviceplzareahistory`, `serviceplzareacustomer`, `serviceplzareamapping`, `serviceplzareaneighbour` +- aus `serviceradius` +- aus `parameter` und `metatype` + +#### Was passiert mit den Daten? + +- Preis- und Rabattzeilen werden gelesen, geaendert oder neu historisiert. +- Kundenoverrides werden getrennt von der allgemeinen Historie gepflegt. +- PLZ- und Bereichszuordnungen werden fuer die spaetere Preisaufloesung vorbereitet. +- Fahrzeugtyp-Mapping und Nachbarrelationen beeinflussen die spaetere Preis- und Dispositionslogik. + +#### Wohin werden die Daten geschrieben? + +- nach `servicehistory` +- nach `servicecustomer` +- nach `serviceplzhistory` +- nach `serviceplzcustomer` +- nach `serviceplzneighbour` +- nach `serviceplzareahistory` +- nach `serviceplzareacustomer` +- nach `serviceplzareamapping` +- nach `serviceplzareaneighbour` +- nach `serviceradius` + +#### Wodurch wird der Ablauf gesteuert? + +- `MANDATOR_SERVICE_ENABLED` +- `MANDATOR_SERVICE2_ENABLED` +- `SERVICE_DISPLAY_MODE_DEFAULT` +- `SERVICE_VEHICLE_TYPE_ENABLED` +- `RIGHTS_TABLE_SERVICE` +- `MASK_EXCLUDE_VHT_IDS` +- `CUSTOMER_SERVICE_RADIUS_INTERVALS_` + +### 3.7 Adress-, Geo-, Feiertags-, Anfahrtszeit-, Gruppen-, Filter- und Formularpflege + +#### Was passiert? + +Stammadressen, Strassenkatalog, Fahrzeiten je PLZ und HQ, Feiertage, Gruppenkataloge, Kurierfilter und dynamische Formulare werden gepflegt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- den passenden Pflegebereich fuer Adresse, Fahrzeit, Feiertag, Gruppe, Filter oder Formular oeffnen +- die Stammdaten mit eindeutigen und vollstaendigen Werten erfassen oder bereinigen +- bei Filtern und Formularen die spaetere Nutzung fuer Kunden, Kuriere oder Fahrzeuge beachten +- Aenderungen speichern und bei Filterloeschungen die Folgen fuer bestehende Zuordnungen pruefen + +#### Welche Eingabefelder sind zu fuellen? + +- allgemeine Adressfelder wie Strasse, Hausnummer, PLZ, Ort und Land +- Fahrzeitfelder je Start-/Ziel-PLZ oder Matrixzelle (`zipcodeFrom`, `zipcodeTo`, `f_`) +- Feiertagsfelder wie Datum, Zeitraum und Bezeichnung (`f_year`, `f_month`, `f_day`, `f_time_from`, `f_time_to`) +- Filterfelder (`f_crf_type`, `f_crf_status`, `f_crf_short`, `f_crf_text`, `f_crf_sort`) +- Metafeld- und Formularfelder (`f_mtfc_mnemonic`, `f_mtfc_description`, `f_mtfk_type`, `f_mtfk_name`, `f_mtft_name`, `f_filter_mtfk_name`) + +#### Woher kommen die Daten? + +- aus `address` +- aus `phoenix_special.street` +- aus `serviceplz`, `serviceplztraveltime` +- aus `publicholiday` +- aus `groups`, `courierfilter` +- aus `metafieldcategory`, `metafieldkey`, `metafieldtemplate`, `metafieldcategorykey`, `metafieldvalue` +- aus `customer`, `courier`, `company` + +#### Was passiert mit den Daten? + +- Adressen und Strassen werden angelegt, bereinigt oder gesucht. +- Anfahrtszeiten werden HQ-bezogen gepflegt. +- Feiertage werden jahresbezogen gepflegt. +- Gruppen und Filter werden als Stammdaten fuer andere Module bereitgestellt. +- Metafeldkategorien, Templates und Werte definieren flexible Formulare und Sondermasken. +- Beim Loeschen von Filtern werden bestehende Kunden-, Kurier- und Fahrzeugzuordnungen bereinigt. + +#### Wohin werden die Daten geschrieben? + +- nach `address` +- nach `serviceplztraveltime` +- nach `publicholiday` +- nach `groups` +- nach `courierfilter` +- nach `metafieldcategory` +- nach `metafieldkey` +- nach `metafieldtemplate` +- nach `metafieldcategorykey` +- nach `metafieldvalue` +- bei Filterbereinigung zusaetzlich nach `customer.cs_filter`, `courier.cr_filter`, `couriervehicle.crvh_filter` + +#### Wodurch wird der Ablauf gesteuert? + +- `MASK_ZIP_CHK_SYNTAX_DISABLED` +- `FILTER_TO_BE_DELETED` +- `MASK_CONTACT_*` +- `MTFC_TPL_MODE` +- `MASK_FORMS_CR` +- `SIMILARITY_SEARCH_FILTER_HQ` + +### 3.8 Auftragserfassung, Auftragsaenderung und Batch-Erfassung + +#### Was passiert? + +Auftraege werden einzeln oder als Batch angelegt, geaendert, storniert, kopiert oder als Kinder-/Kettenauftrag fortgeschrieben. Dabei werden Stationen, Services, Preise, Artikel, Zahler und Status verdrahtet. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- Auftraggeber, Kostenstelle und Abrechnungsbezug korrekt auswaehlen +- alle benoetigten Stationen, Zeiten, Services, Artikel und Zusatzinformationen erfassen +- Pflichtpruefungen fuer Adresse, Preis, Zahlart und Status aufloesen +- den Auftrag speichern und bei Bedarf direkt disponieren, kopieren oder als Batch verarbeiten + +#### Welche Eingabefelder sind zu fuellen? + +- Auftraggeber- und Zahlerfelder (`csc_id_orderer`, `csc_id_payer`, `jb_orderer`, `jb_commission_no`) +- Termin- und Ablaufdaten (`tag`, `monat`, `jahr`, `stunde`, `minute`, `endetag`, `endemonat`, `endejahr`, `jb_warn*`, `jb_waittime_*`) +- Fahrzeug-, Sendungs- und Leistungsdaten (`vht_id`, `jb_weight`, `jb_crvh_length`, `jb_crvh_width`, `jb_crvh_height`, `jb_crvh_position`, Services, Artikel) +- Preis- und Zusatzkostenfelder (`jb_fixprice`, `jb_serviceprice`, `jb_cr_price`, `jb_cr_serviceprice`, `jb_discount`, `jb_discount_rate`, `jb_markup`, `jb_cr_markup`, `jb_toll`, `jb_km`, `jb_value_of_goods`, `jb_insurance_rate`) +- Steuerungsfelder fuer Status und Abwicklung (`jb_type`, `jb_globaljob`, `jb_status`, `jb_status_manual`, `jb_freetext_1`, `jb_hiddenFreetext_1`, `jb_costsplit`, `jb_permanent`, `jb_multi_factor`, `jb_cash`, `jb_offer`, `jb_origin`, `jb_origin_other`, `jb_dispoinfo`) +- Stationsfelder in den Tourmasken wie Name, Person, Strasse, Hausnummer, PLZ, Ort, Land, Bemerkung, Kommissionsnummer und Stationsstatus + +#### Woher kommen die Daten? + +- aus `job`, `tour`, `tourservice`, `jobprice` +- aus `jobbatch`, `jobbatchlist` +- aus `customer`, `costcenter`, `costcenteraddress`, `address` +- aus `service*`, `serviceplz*`, `serviceplzarea*` +- aus `metatype`, `tax`, `headquarters` +- aus `genericdatacontainer` +- aus `phoenix_log.semaphor`, `phoenix_log.log` + +#### Was passiert mit den Daten? + +- Auftragskopf, Stationen und Preispositionen werden validiert und zusammengesetzt. +- Adressen werden wiederverwendet oder neu angelegt. +- Preise werden ueber Service-, PLZ-, Bereichs- und Spezialregeln berechnet. +- Jobstatus, Langstreckenflag und Dispositionsrelevanz werden gesetzt. +- Kundenseitige Stornos erzeugen Statuswechsel, Gegenbelege und Logeintraege. +- Batch-Erfassung verdichtet Listenvorlagen zu mehreren Auftraegen. + +#### Wohin werden die Daten geschrieben? + +- nach `address` +- nach `job` +- nach `tour` +- nach `tourservice` +- nach `jobprice` +- nach `jobbatch` +- nach `jobbatchlist` +- nach `genericdatacontainer` +- nach `phoenix_log.semaphor` +- nach `phoenix_log.log` +- in Einzelfaellen nach `user.usr_inv_email` +- in Einzelfaellen nach `company.cmp_postage` + +#### Wodurch wird der Ablauf gesteuert? + +- `MASK_SINGLE_JOBEDIT` +- `MASK_MANUAL_DISPOSITION` +- `MASK_MANUAL_DISPOSITION_CUSTOMER_MANDATORY` +- `MODE_COPY_JOB` +- `MODE_LATER_JOB` +- `GLOBAL_USE_RELATED_CUSTOMER` +- `CUSTOMER_MASK_JB_CREATOR_DISCOUNT_` +- `MASK_CS_SELF_SERVICE_DISCOUNT` +- `MASK_MANDATORY_FILTERS` +- `MASK_CS_DONT_MAKE_STANDARD_PRICE_` +- `CUSTOMER_MASK_CALCULATOR_USAGE_ONLY_` +- `COSTCENTER_INV_MODE_` +- `EU_COUNTRYCODES` +- `MASK_CR_PRICE_MODE` +- `MASK_CR_PRICE_MODE_DATE` +- `INV_SERVICEPRICE_DISCOUNT` +- `CS_JB_JAM_WAITTIME_*` +- `MASK_INSERTADDRESS_DISTRICT_` +- `MASK_TR_PHOTO_FORCE` +- `MASK_MIN_MAX_TR_PHOTO_FORCE` +- `JB_EDITBATCH_*` +- `LONGHAUL_ACTIVE` +- `LONGHAUL_KM` + +### 3.9 Disposition, Ranking, Favoriten, Push und Longhaul + +#### Was passiert? + +Offene Auftraege werden manuell oder automatisch Kuriere zugeordnet. Dabei spielen Fahrzeugtyp, Verfuegbarkeit, Favoriten, Sperren, Gebiete, Ortung und Langstreckenlogik zusammen. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- einen offenen Auftrag und einen fachlich passenden Kurier auswaehlen +- Ranking, Favoriten, Sperren, Fahrzeugtyp und Verfuegbarkeit pruefen +- die Zuordnung, Rueckgabe oder Vermittlung bewusst bestaetigen +- bei Bedarf Push oder Autoranking ausloesen und das Ergebnis kontrollieren + +#### Welche Eingabefelder sind zu fuellen? + +- Auftragsreferenz (`jb_id`) +- Kurierauswahl und Reihenfolge (`cr_id_order`, `cr_id_orders`, `cr_sid`) +- Filter- und Favoritensteuerung (`jb_cr_filter`, `jb_cr_filter_opt`, `ignore_fav_only`) +- manuelle Dispositionsangaben (`jb_status_manual`, `jb_dispoinfo`, `jb_jam_waittime`) +- Push-, Rueckgabe- oder Vermittlungsaktion im jeweiligen Dispositionsfenster + +#### Woher kommen die Daten? + +- aus `job`, `tour`, `address` +- aus `courier`, `couriervehicle` +- aus `customercourier` +- aus `serviceplz`, `serviceplzneighbour`, `serviceplzarea`, `serviceplzareamapping`, `serviceplzareaneighbour` +- aus `metatype`, `headquarters` +- aus `phoenix_log.locating` +- aus `parameter` + +#### Was passiert mit den Daten? + +- offene Jobs werden nach Rankingregeln bewertet +- Favoriten- und Sperrbeziehungen werden geprueft +- manuelle Rueckgabe in die Vermittlung oder in manuelle Disposition wird ausgefuehrt +- bei Langstrecken werden zusaetzliche HQ- und Distanzregeln geprueft +- fuer entfernte oder unplausible PDA-Zustaende werden Warnungen abgeleitet + +#### Wohin werden die Daten geschrieben? + +- nach `job.cr_id` +- nach `job.cr_id_order` +- nach `job.cr_sid` +- nach `job.jb_status` +- nach `job.jb_globaljob` +- nach `job.jb_longhaul` +- nach `genericdatacontainer` mit `jb_ignore_fav_only` +- nach `autoranking` +- nach `phoenix_pda.commandexec` +- nach `phoenix_log.log` + +#### Wodurch wird der Ablauf gesteuert? + +- `AUTORANKING_*` +- `RANKING_*` +- `MODE_INTERMEDIATION` +- `MASK_COURIER_SORT_BY_OCCUPIED` +- `MASK_MANDATORY_FILTERS` +- `LOCATING_*` +- `LONGHAUL_ACTIVE` +- `LONGHAUL_ACTIVE_REMOTE_DB` +- `LONGHAUL_KM` +- `BWV_CR_PRICE_RETOUR` + +### 3.10 Karten, Ortung, Strecken und Tracking-Ausgaben + +#### Was passiert? + +Kartenansichten, Kurierortung, Distanzberechnung, Toursortierung, Trackingabfragen und partnerbezogene Trackingformate werden bereitgestellt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- den richtigen Auftrag, Kurier, Zeitraum oder Trackingkontext auswaehlen +- Karten-, Ortungs- oder Routenansicht gezielt aufrufen +- bei Sortierung oder Distanzpruefung die vorgeschlagenen Ergebnisse fachlich kontrollieren +- die benoetigte Ausgabe oder Trackingansicht fuer den weiteren Prozess verwenden + +#### Welche Eingabefelder sind zu fuellen? + +- Auftrags- oder Kurierreferenz (`jb_id`, `cr_sid`) +- Trackingcode und Sprache (`trackingID`, `selectedLanguage`) +- Kartenkontext oder Aktualisierungsaktion fuer die Live-Ansicht +- bei Routenneuberechnung die Auswahl der betroffenen Tour oder Anzeigevariante + +#### Woher kommen die Daten? + +- aus `headquarters`, `courier`, `job`, `tour`, `address`, `stock` +- aus `phoenix_log.route` +- aus `tourarticle`, `tourarticleprocess` +- aus `address_geo.address_geo`, `address_geo.distance` +- aus `genericdatacontainer` + +#### Was passiert mit den Daten? + +- HQ- und Kurierpositionen werden auf Karten verdichtet. +- Adressen werden geocodiert oder aus dem Cache gelesen. +- Entfernungen und Fahrzeiten werden berechnet oder aus dem Distanzcache gelesen. +- Tourreihenfolgen werden optimiert und umgeschrieben. +- Trackingformate fuer verschiedene Partner werden aus Job- und Stationsdaten erzeugt. + +#### Wohin werden die Daten geschrieben? + +- nach `address` +- nach `address_geo.address_geo` +- nach `address_geo.distance` +- nach `address_geo.distance_osm` +- nach `tour.tr_sort` +- nach `tourarticle.tr_sort` +- nach `job.jb_tourdata` +- nach `jobprice` mit `mt_sort = 11` +- nach `courier.cr_gps_*` + +#### Wodurch wird der Ablauf gesteuert? + +- `MASK_JB_MAP_VIEW_ENABLED_` +- `MASK_JB_MAP_VIEW_COURIERS_` +- `GEOCACHE_LIFETIME` +- `LOCATING_MODE` +- `LOCATING_LBS_*` +- `LOCATING_ZIPCODE_COMPARISON_MODE` +- `LOCATING_PLAUSIBLE_VELOCITY` +- `PZM_ROUNDTRIPKM` +- `PZM_SHORTEST` +- `GEO_EARTH_RADIUS` +- `CO2_FORMULAR_*` + +### 3.11 Auftragsdetail, POD, Signatur, Mail und PDF-Detailausgabe + +#### Was passiert? + +Im Auftragsdetail werden alle fachlichen Detaildaten angezeigt, Signaturen und Fotos verarbeitet, Statusmails erzeugt und PDF-Ausgaben pro Auftrag oder Rechnungsnummer erzeugt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- den richtigen Auftrag oder die richtige Rechnungsnummer oeffnen +- POD-Daten, Fotos und Signaturen pruefen oder nachreichen +- bei Mailversand Empfaenger, Sprache und Inhalt fachlich kontrollieren +- die gewuenschte Detail-, Mail- oder PDF-Aktion aktiv ausloesen + +#### Welche Eingabefelder sind zu fuellen? + +- Auftragsreferenz und Historienkontext (`job_id`, `dbhistory`) +- Empfaenger- und Buchungsfelder (`f_email`, `f_crvh_sid_book`, `f_crvh_sid_order`, `f_jbp_*`) +- Detail- und Zusatzfelder (`f_tourname`, `f_jb_incomplete`, `f_jb_freetext_3_new`, `f_jb_finishtime4booking`) +- interne Bemerkung, POD-/Foto-/Signaturbezug und ausgeloeste Mail- oder PDF-Aktion (`f_int_rem_text`) + +#### Woher kommen die Daten? + +- aus `job`, `tour`, `tourservice`, `jobprice` +- aus `courier`, `customer`, `costcenter`, `company` +- aus `genericdatacontainer` +- aus `phoenix_log.route`, `phoenix_log.log` +- aus `invoice` +- aus vorhandenen Bild- und Signaturdateien + +#### Was passiert mit den Daten? + +- Detaildaten werden fuer Anzeige, Mail und PDF zusammengesetzt. +- Signaturen und POD-Fotos werden dem Auftrag zugeordnet. +- Mails werden mit kundenspezifischen Texten, Logos und Sprachregeln aufgebaut. +- PDF-Ausgaben aggregieren mehrere Auftraege ueber eine Rechnungsnummer. + +#### Wohin werden die Daten geschrieben? + +- in `html/temp/signs/` +- in `html/temp/photos/` +- in temporaere PDF- und Downloadpfade +- in Mail- und Automailer-Logs +- in Einzelfaellen nach `genericdatacontainer` + +#### Wodurch wird der Ablauf gesteuert? + +- `MASK_JOBDETAILS_*` +- `MAIL_*` +- `JOBDETAILS_EMAIL_LANGUAGE_` +- `JOB_MAIL_STATION_ARRIVAL_TIME_` +- `AUTOMAILER_*` + +### 3.12 Rechnungswesen und Rechnungszuordnung + +#### Was passiert? + +Auftraege werden Rechnungsnummern und Rechnungsdaten zugeordnet. Aus diesen Zuordnungen entstehen Rechnungsuebersichten, Kurieransichten und PDF-Sammelausgaben. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- die abzurechnenden Auftraege oder die Zielrechnung eindeutig auswaehlen +- Rechnungsnummer, Rechnungsdatum und Zusatzangaben korrekt pflegen +- bei Bedarf Kurierbemerkungen oder Zahlungsbezug ergaenzen +- die Zuordnung speichern und die Rechnungs- oder PDF-Ausgabe pruefen + +#### Welche Eingabefelder sind zu fuellen? + +- Datumsbereich (`day_from`, `month_from`, `year_from`, `day_to`, `month_to`, `year_to`) +- Kostenstellen- oder Kurierbezug (`jb_costcenter`, `cr_sid`) +- Kundenfilter (`cs_eid`, `cmp_name`) +- Status- und Anzeigeoptionen (`jb_status`, `show_invoice_text`) +- in der Detailpflege zusaetzlich Rechnungsnummer, Rechnungsdatum und Bemerkungen + +#### Woher kommen die Daten? + +- aus `invoice` +- aus `job`, `tour`, `jobprice` +- aus `costcenter`, `customer`, `employee`, `user`, `courier` +- aus `jobpayment`, `jobpaymentcollection` + +#### Was passiert mit den Daten? + +- mehrere Jobs werden einer Rechnungsnummer zugeordnet +- Rechnungsdatum und Rechnungsnummer werden je Job gesetzt oder aktualisiert +- Kurierbemerkungen koennen gepflegt werden +- fuer die PDF-Ausgabe werden alle Jobs einer Rechnungsnummer geladen + +#### Wohin werden die Daten geschrieben? + +- nach `invoice` +- nach `job.jb_cr_remark` + +#### Wodurch wird der Ablauf gesteuert? + +- `INV_*` +- `JB_PAYMODE_CASH` +- `JB_TAX_RATE_SIGN` + +### 3.13 Statistik und Kennzahlen + +#### Was passiert? + +Die Statistik stellt Kennzahlen zu Jobs, Kunden, Kurieren, Services, Preisen, Surveys und Umsaetzen bereit. Der Schwerpunkt liegt auf Selektion, Aggregation und Gruppierung. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- Zeitraum, HQ-Kontext und die fachlich passenden Filter setzen +- die benoetigte Kennzahlensicht oder Gruppierung auswaehlen +- die Auswertung starten und auf Plausibilitaet pruefen +- das Ergebnis bei Bedarf exportieren oder fuer Folgeprozesse verwenden + +#### Welche Eingabefelder sind zu fuellen? + +- Zeitraum mit Datum und optional Uhrzeit (`day_*`, `month_*`, `year_*`, `hour_*`, `minute_*`) +- Statistiktyp, Kategorie, Sortierung und Datumsmodus (`f_category`, `f_statistic`, `f_sort`, `f_direction_sort`, `f_dateMode`, `f_statusMode`, `f_priceMode`) +- Objektfilter fuer Kunde, Kurier, Fahrzeug und HQ (`g_cs_eid`, `g_cr_eid`, `f_crvh_sid`, `f_hq_id`, `f_filter*`) +- Leistungs-, Service-, Gruppen- und Groessenfilter (`f_service`, `f_servicetype`, `f_jb_service`, `f_jb_specifics`, `f_group`, `f_staticGroup`, `f_filter_jb_*`) +- Ausgabefelder fuer Datei, Diagramm, Netto/Brutto und Zusatzdaten (`fileOutput`, `f_type_chart`, `f_net_gross`, `f_show_*`) + +#### Woher kommen die Daten? + +- aus `job`, `tour`, `jobprice` +- aus `courier`, `couriervehicle` +- aus `customer`, `costcenter`, `company`, `branch` +- aus `user` +- aus `genericdatacontainer` +- aus `phoenix_log.log` +- aus Archivtabellen ueber `getDBNames` + +#### Was passiert mit den Daten? + +- Zeitraeume und HQ-Filter werden aufgeloest. +- Auftraege, Kunden und Kuriere werden nach vielen Kriterien gefiltert. +- Kennzahlen werden gruppiert, summiert und fuer UI oder Dateioutput aufbereitet. +- Survey-, Preis- und Zusatzdaten werden optional in die Statistik eingeblendet. + +#### Wohin werden die Daten geschrieben? + +- standardmaessig nirgends in die Datenbank +- optional in Dateioutput fuer Exporte oder Druckausgabe + +#### Wodurch wird der Ablauf gesteuert? + +- `GLOBAL_USE_RELATED_CUSTOMER` +- `MASK_STATISTIC_*` +- `STATISTIC_DATE_MODE` +- `STATISTIC_INTERVALS_*` +- `STATISTIC_YEAR_OF_BEGIN_HISTORY` +- `STATISTIC_NO_STORNOJOBS` +- `STATISTIC_EXCLUDED_CS` +- `STATISTIC_CSEIDS_EXCLUDED*` +- `STATISTIC_CREIDS_EXCLUDED*` +- `STATISTIC_CRVHSIDS_EXCLUDED` +- `STATISTIC_PREFERED_CATEGORY_INDEX_JB` + +### 3.14 Vertrieb, Termine, Aufgaben und Berichte + +#### Was passiert? + +Termine, Wiedervorlagen, Aufgaben und Vertriebsberichte fuer Kunden, Transporteure und Interessenten werden geplant, angezeigt und abgeschlossen. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- den richtigen Objektbezug wie Kunde, Transporteur oder Interessent auswaehlen +- Termin, Aufgabe oder Bericht mit Datum, Teilnehmern und Inhalt pflegen +- den Bearbeitungsstatus aktuell halten und offene Punkte dokumentieren +- nach Abschluss den Termin oder Bericht sauber abschliessen + +#### Welche Eingabefelder sind zu fuellen? + +- Termin- und Wiedervorlagefelder (`f_day`, `f_month`, `f_year`, `f_hour`, `f_minute`, `f_day_to`, `f_month_to`, `f_year_to`, `f_hour_to`, `f_minute_to`) +- Kategorien und Verantwortliche (`f_ap_cat_1` bis `f_ap_cat_4`, `f_selUsrId`, `f_grp_id`) +- Objekt- und Kundenbezug (`g_cs_eid` sowie Objektreferenzen aus Kunde, Transporteur oder Interessent) +- Inhalt und Teilnehmer (`f_text`, Teilnehmerauswahl, Gruppenwahl) +- Benachrichtigungsfelder (`f_sendmail[]`, `f_sendmail_cs[]`, `f_emailAdrCs`, `f_mail_salutation`, `f_mail_greetings`) + +#### Woher kommen die Daten? + +- aus `phoenix_group.appointment` +- aus `phoenix_group.report_process` +- aus `user`, `employee` +- aus `customer`, `courier`, `company`, `headquarters` +- aus `metatype` + +#### Was passiert mit den Daten? + +- Termine werden angelegt, aktualisiert oder geloescht. +- Teilnehmerlisten und Sichtbarkeiten werden ausgewertet. +- Aufgaben sind fachlich Sonderfaelle von Terminen. +- Berichte werden objektspezifisch gespeichert und koennen gleichentags offene Termine auf erledigt setzen. + +#### Wohin werden die Daten geschrieben? + +- nach `phoenix_group.appointment` +- nach `phoenix_group.report_process` + +#### Wodurch wird der Ablauf gesteuert? + +- `MASK_AP_BTN_*` +- `MASK_AP_CATEGORY_*` +- `MASK_AP_CAT_*` +- `MASK_AP_WARNING_DISABLE_QUESTION_NO_REPORT_OF_SAME_DAY` +- `MASK_CS_REPORT_*` +- `MASK_CR_REPORT_*` +- `MASK_PT_REPORT_*` +- `MASK_RP_ENABLE_DEACTIVATION_APPOINTMENT_WARNING_OF_SAME_DAY` +- `IMG_LOGO_*` + +### 3.15 Kommunikation, Mitteilungen und Endgeraetehistorie + +#### Was passiert? + +HQ-interne Ticker-Meldungen, Kuriernachrichten, Zustellbestaetigungen, Antworten und Nachrichtengruppenzuordnungen werden verwaltet. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- die passende Zielgruppe, den Kurier oder die Nachrichtengruppe auswaehlen +- den Nachrichtentext fachlich eindeutig formulieren +- Versand oder Veroeffentlichung gezielt ausloesen +- Rueckmeldungen, Antworten und Lesestatus im Nachgang kontrollieren + +#### Welche Eingabefelder sind zu fuellen? + +- Tickerfelder wie Geltungszeitraum, Betreff, Text und Sichtbarkeit (`day_from`, `month_from`, `year_from`, `day_to`, `month_to`, `year_to`, `f_tif_subject`, `f_tif_text`, `f_tif_unerasable`, `f_hq_id_insert`, `f_emp_id_insert`) +- Nachrichtenfilter wie Zeitraum, Prioritaet, Status und Empfaengertyp (`day_*`, `month_*`, `year_*`, `f_prio`, `f_state`, `f_usr_type`, `f_filter`) +- Gruppen- und Einzelnachrichtenfelder (`f_msggrp`, `f_subject_group`, `f_body_group`, `f_usr_sender`, `f_usr_receiver`, `f_cr_sid_msg`) +- Zuordnung von Nachrichtengruppen zu Kurieren (`f_msggrp` in der Kurierpflege) + +#### Woher kommen die Daten? + +- aus `phoenix_group.tickerforum` +- aus `phoenix_log.messageforum` +- aus `messagegroup` +- aus `courier`, `user`, `company`, `headquarters`, `employee` + +#### Was passiert mit den Daten? + +- Ticker-Nachrichten werden erstellt, ausgesteuert und abgelaufene Eintraege geloescht. +- Nachrichten an Fahrer oder Fahrergruppen werden erzeugt. +- Lese- und Antwortstatus werden nachgefuehrt. +- Nachrichtengruppen werden einzelnen Kurieren zugewiesen. + +#### Wohin werden die Daten geschrieben? + +- nach `phoenix_group.tickerforum` +- nach `phoenix_log.messageforum` +- nach `courier.cr_msggrp` + +#### Wodurch wird der Ablauf gesteuert? + +- `NEWSTICKER_MAX_BODY_LENGTH` +- `MESSAGE_MAX_BODY_LENGTH` +- `MESSAGE_MIN_DAYS_BEFORE` + +### 3.16 Datenexport, Datentransfer, Dokumente und Dateistaging + +#### Was passiert? + +Stamm- und Bewegungsdaten werden exportiert, Dokumente werden hochgeladen oder heruntergeladen, FTP/SFTP-Transfers werden ausgefuehrt und Importdateien werden bereitgestellt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- das richtige Exportprofil, Zielsystem oder Uploadziel auswaehlen +- Dateien oder Objektmengen fachlich korrekt zusammenstellen +- bei externem Transfer Zugangsdaten, Zielpfade und Dateinamenskonventionen beachten +- den Transfer starten und Ergebnis, Protokolle und Ablage pruefen + +#### Welche Eingabefelder sind zu fuellen? + +- Exportkategorie, Exportmodus und Parameterprofil (`f_exp_category`, `f_export_mode`, `f_parname`, `f_parname_export`, `f_expp_id`) +- Dateiaufbau und Dateiname (`f_fileName`, `f_delimiter`, `f_fillUpChar`, `f_headline`, `f_bolchars`, `f_eolchars`, `f_bofchars`, `f_eofchars`) +- Datenfilter (`day_from`, `month_from`, `year_from`, `day_to`, `month_to`, `year_to`, `f_cs_eid_filter`, `f_status_filter`, `f_vht_filter`, `f_jbp_filter`, `f_csId`, `f_grpId`) +- Administrationsfelder fuer Parameterpflege und Dateiloeschung (`f_parname_new`, `f_exportFileToDelete`, `adminInterfacePassword`) + +#### Woher kommen die Daten? + +- aus `exportparameters`, `exportfiles`, `exportcategory*` +- aus `job`, `jobpayment`, `jobpaymentcollection` +- aus `company`, `couriervehicle`, `customer` +- aus dem lokalen Dateisystem unter `import/upload/` +- aus entfernten FTP-/SFTP-Verzeichnissen + +#### Was passiert mit den Daten? + +- Exportprofile bestimmen Struktur und Inhalt der Dateien. +- Exportdateien werden generiert und archiviert. +- Uploads werden objektbezogen in statische oder HQ-bezogene Verzeichnisse einsortiert. +- FTP-Serverlisten und Remoteziele werden aus Parametern aufgeloest. +- Exportmarkierungen an Firmen, Fahrzeugen und Jobs verhindern Doppelverarbeitung oder dokumentieren den Exportzeitpunkt. + +#### Wohin werden die Daten geschrieben? + +- in `html/export/download/` +- in `html/import/upload/...` +- nach `exportfiles` +- nach `company.cmp_export_time` +- nach `couriervehicle.crvh_export_time` +- nach `job.jb_export_time` +- nach `jobpayment.jbp_export_time` +- nach `jobpaymentcollection.jbpc_export_time` + +#### Wodurch wird der Ablauf gesteuert? + +- `EXPORT_PATH` +- `EXPORT_FILES_ON_SERVER` +- `EXPORT_FILES_ON_SERVER_CUSTOMER` +- `EXPORT_SEMAPHORE_*` +- `EXPORT_CSV_HEADLINE_CS_` +- `EXPORT_CAT_09_FILE_EXTENSION` +- `EXPORT_MASK_FTP_*` +- `EXPORT_FTP_FILE_EXTENSIONS_ENABLED` +- `FTP_IMPORT_SERVERLIST` +- `FTP_SERVER_*` +- `FTP_USER_*` +- `FTP_PASSWORD_*` +- `FTP_REMOTEPATH_*` +- `DATATRANSFER_DIRECTORY_*` +- `DATATRANSFER_HQ_PATH_*_ENABLED` +- `DATATRANSFER_MAX_*` +- `DATATRANSFER_UPLOAD_PREFIX_RENAME` +- `MASK_DATATRANSFER_NAV2ROOTDIR_ENABLED` + +### 3.17 XML-, Mobile- und Partner-Schnittstellen + +#### Was passiert? + +Externe Systeme koennen Kunden, Transporteure, Auftraege, Stationen, Preise, Filter, Metadaten, Groupware-Daten und mobile Zeit- oder Positionsmeldungen ueber XML austauschen. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- als technischer Benutzer den richtigen Endpunkt und den passenden Mandantenkontext verwenden +- Requests mit korrekten IDs, Hashes, Pflichtfeldern und Nutzdaten senden +- Rueckmeldungen fachlich auswerten und Fehlerfaelle sauber behandeln +- bei Produktivnutzung die erzeugten XML-Logs und Seiteneffekte kontrollieren + +#### Welche Eingabefelder sind zu fuellen? + +- Request-Nutzlast als XML (`orderReq`, `customerReq`, `contractorReq`) +- Sprach- und Aktionsfelder (`selectedLanguage`, `f_act`) +- Authentifizierungsfelder in XML wie `auth_type`, `auth_id`, `auth_eid`, `account`, `password`, `session_id` +- Objektfelder in XML wie Kunde/Kostenstelle, Transporteur/Fahrzeug, Station, Gruppe, Service, Trackingcode oder Operation + +#### Woher kommen die Daten? + +- aus XML-Requests ueber `service/*.php` +- aus `customer`, `costcenter`, `costcenteraddress`, `company` +- aus `courier`, `couriervehicle`, `user`, `address` +- aus `job`, `tour`, `tourservice`, `jobprice` +- aus Preis- und Gebietsstammdaten +- aus `courierfilter` +- aus `phoenix_group.timetracking` +- aus `meta_object.metaobject` + +#### Was passiert mit den Daten? + +- Requests werden geparst, authentifiziert und in Fachobjekte zerlegt. +- Fachlich relevante IDs wie `cs_id`, `cr_id`, `csc_id`, `jb_id` oder externe Hashes werden aufgeloest. +- Je nach Endpunkt werden Objekte gelesen, angelegt, aktualisiert, geloescht oder angereichert. +- Alle Requests werden zusaetzlich in XML-Logdateien protokolliert. + +#### Wohin werden die Daten geschrieben? + +Je nach Endpunkt unter anderem: + +- nach `company`, `customer`, `costcenter`, `costcenteraddress` +- nach `courier`, `couriervehicle`, `user`, `address` +- nach `job`, `tour`, `tourservice`, `jobprice` +- nach `courierfilter` +- nach `phoenix_group.timetracking` +- nach `meta_object.metaobject` +- in `html/log/*.log` + +#### Wodurch wird der Ablauf gesteuert? + +- `GLOBAL_USE_RELATED_CUSTOMER` +- `GLOBAL_UNIQUE_DB_INSTANCE_NO` +- `HQ_INSTANCE` +- `DISPOSITION_JB_STATUS_MODE` +- `FDS_CUSTOMER_ENABLED_CS_` +- `ORDER_REQUEST_VHT_ID_DEFAULT_` +- `ORDER_REQUEST_INVTEXT_DAYTIME_DISABLED` +- `ORDER_REQUEST_NO_PRICE_CS_` +- `TRACKING_ENABLED_CS_` +- `ORDER_REQUEST_ERR_115_DISABLED` +- `ORDER_REQUEST_AUTORESPONSE_ENABLED_CS_` +- `STATION_REQUEST_ERROR_HANDLER_DISABLED*` +- `STATION_REQUEST_CHECK_EXISTENCE_COMMISSION_NO` +- `STATION_REQUEST_CHECK_OPERATION_EXISTENCE_COMMISSION_NO` +- `SYSTEM_FORM_SINGLE_HQ*` +- `MAXIMUM_LOGIN_TRIALS` +- `CR_ADD_TO_ASSET_ENABLED` + +### 3.18 Lager und Artikelwirtschaft + +#### Was passiert? + +Lagerstrukturen, Artikel, Bestandsmengen, Serien- bzw. Scanvorgaenge, Lagerjournale und Lageradressen werden gepflegt. + +#### Was muss der Benutzer tun, um den Schritt erfolgreich abzuschliessen? + +- das richtige Lager, Unterlager oder den betroffenen Artikel auswaehlen +- Mengen, Bewegungsart, Bezug und gegebenenfalls Scan- oder Serieninformationen erfassen +- die Buchung oder Stammdatenpflege speichern +- das Ergebnis im Lagerjournal oder Bestandsbild fachlich kontrollieren + +#### Welche Eingabefelder sind zu fuellen? + +- Lagerstammdaten wie Maximalbestand, Barcode und Adressdaten (`f_stk_maxquantity`, `f_stk_barcode`, `f_ad_*`, `f_stk_hsno`) +- Such- und Filterfelder fuer Artikel und Lager (`f_stkat_search_at_misc`, `f_stkat_search_atg`, `f_displayModeStockArticle`, `f_mv_mode*`) +- Bewegungsfelder (`f_mv_stk_from`, `f_mv_stk_to`, `f_mv_at`, `f_mv_stk_itemquantity`, `f_mv_tan`, `f_mv_remark`, `f_mv_serialno`, `f_mv_jb_id`) +- frei konfigurierbare Zusatzfelder fuer Lagerbewegungen (`f_mv_datafield_01` bis `f_mv_datafield_15`) +- Scan- und Journalfelder (`f_scan`, `f_stkat_scanmode`, `f_store`, `f_mv_search_*`, Datumsfilter) + +#### Woher kommen die Daten? + +- aus `stock`, `stockarticle`, `article`, `articlegroup`, `articlegroupitem` +- aus `address` +- aus `customer`, `company` +- aus `parameter` + +#### Was passiert mit den Daten? + +- Lagerbaeume und Unterlager werden aufgelistet und gefiltert. +- Artikelzugriffe werden je Mitarbeiter und Lagerkontext eingeschraenkt. +- Bestandsbewegungen erzeugen Journal- und Bewegungsdaten. +- Lageradressen werden ueber die allgemeine Adresslogik gelesen oder gepflegt. + +#### Wohin werden die Daten geschrieben? + +- nach `stock` +- nach `stockarticle` +- nach `articleitem` +- nach `stockmove` +- bei Lageradresspflege nach `address` + +#### Wodurch wird der Ablauf gesteuert? + +- `MASK_STK_ROOT_ACCESS` +- `MASK_STK_READONLY` +- `MASK_STK_READONLY_WHERE_DEFINED_WRITEACCESS` +- `MASK_STK_ARTICLE_ACCESS` +- `MASK_STK_SUBSTOCK_ACCESS` +- `MASK_STKAT_SHOW_PERMANENT_` +- `MASK_STK_JOURNAL_*` +- `MASK_STK_DATAFIELDS_*` +- `MASK_LINK_DISPOSITION_STKTO_EQ_STKFROM_` +- `MASK_AT_LIST_COLS` + +--- + +## 4. Wichtigste fachliche Schreibziele im Ueberblick + +Wenn die Frage nicht pro Prozess, sondern uebergreifend gestellt wird, schreibt die Legacy-PHP-Anwendung fachlich vor allem in diese Speicherbereiche: + +- Stamm- und Bewegungsdaten: `user`, `employee`, `company`, `customer`, `costcenter`, `courier`, `couriervehicle`, `job`, `tour`, `tourservice`, `jobprice`, `invoice`, `stock*` +- Konfiguration: `parameter` +- flexible Objektzusaetze: `genericdatacontainer` +- Audit und technische Steuerung: `phoenix_log.log`, `phoenix_log.semaphor`, `phoenix_log.locating`, `phoenix_log.route`, `phoenix_pda.commandexec` +- Kommunikation und Vertrieb: `phoenix_group.appointment`, `phoenix_group.report_process`, `phoenix_group.tickerforum`, `phoenix_log.messageforum` +- Geo- und Routingcache: `address_geo.address_geo`, `address_geo.distance`, `address_geo.distance_osm` +- externe Objektkopplung: `meta_object.metaobject` +- Dateisystem: `import/upload/`, `export/download/`, `temp/download/`, `temp/pdf/`, `temp/photos/`, `temp/signs/`, `images/external/`, `log/` + +--- + +## 5. Zusammenfassung + +Die Legacy-PHP-Anwendung ist fachlich ein komplettes Betriebsportal und kein einzelner Auftragserfassungsdialog. Die Fachlogik verteilt sich ueber: + +- klassische relationale Tabellen +- die Parametrisierung in `parameter` +- flexible Zusatzdaten in `genericdatacontainer` +- Protokoll- und Nebensteuerung in `phoenix_log.*` +- Dateiablagen, Exportpfade und XML-Endpunkte + +Fuer Migration und Analyse ist deshalb entscheidend, jeden Vertikalschnitt nicht nur ueber seine Haupttabellen, sondern immer auch ueber Parameter, Zusatzspeicher, Logs und Dateipfade zu betrachten. diff --git a/html/DOKUMENTATION_NIEDERLASSUNGEN.md b/html/DOKUMENTATION_NIEDERLASSUNGEN.md deleted file mode 100644 index b9b7d32..0000000 --- a/html/DOKUMENTATION_NIEDERLASSUNGEN.md +++ /dev/null @@ -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 | 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`* diff --git a/services/pom.xml b/services/pom.xml index e2a4cb0..9cf7cb2 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -42,6 +42,16 @@ org.springframework.boot spring-boot-starter-mail + + commons-net + commons-net + 3.11.1 + + + com.github.mwiede + jsch + 0.2.20 + com.github.librepdf openpdf diff --git a/services/src/main/java/de/votian/services/config/LegacySchedulingProperties.java b/services/src/main/java/de/votian/services/config/LegacySchedulingProperties.java new file mode 100644 index 0000000..507f33b --- /dev/null +++ b/services/src/main/java/de/votian/services/config/LegacySchedulingProperties.java @@ -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; + } +} diff --git a/services/src/main/java/de/votian/services/config/SchedulingConfig.java b/services/src/main/java/de/votian/services/config/SchedulingConfig.java new file mode 100644 index 0000000..f958edb --- /dev/null +++ b/services/src/main/java/de/votian/services/config/SchedulingConfig.java @@ -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 { +} diff --git a/services/src/main/java/de/votian/services/controller/JobController.java b/services/src/main/java/de/votian/services/controller/JobController.java index 97f9528..24110d4 100644 --- a/services/src/main/java/de/votian/services/controller/JobController.java +++ b/services/src/main/java/de/votian/services/controller/JobController.java @@ -242,6 +242,32 @@ public class JobController { return ResponseEntity.ok(jobService.findTodaysJobs()); } + @GetMapping("/today/count") + public ResponseEntity> 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 create(@RequestBody JobCreateRequest request, Authentication authentication) { UserSessionDto user = sessionUser(authentication); diff --git a/services/src/main/java/de/votian/services/entity/Company.java b/services/src/main/java/de/votian/services/entity/Company.java index 53f8370..4febfe5 100644 --- a/services/src/main/java/de/votian/services/entity/Company.java +++ b/services/src/main/java/de/votian/services/entity/Company.java @@ -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") diff --git a/services/src/main/java/de/votian/services/entity/CostCenter.java b/services/src/main/java/de/votian/services/entity/CostCenter.java index fbd73dd..aa19d0a 100644 --- a/services/src/main/java/de/votian/services/entity/CostCenter.java +++ b/services/src/main/java/de/votian/services/entity/CostCenter.java @@ -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; } } diff --git a/services/src/main/java/de/votian/services/entity/Employee.java b/services/src/main/java/de/votian/services/entity/Employee.java index 1cc70d5..a2370f9 100644 --- a/services/src/main/java/de/votian/services/entity/Employee.java +++ b/services/src/main/java/de/votian/services/entity/Employee.java @@ -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") diff --git a/services/src/main/java/de/votian/services/entity/GroupwareAppointment.java b/services/src/main/java/de/votian/services/entity/GroupwareAppointment.java index 26bbc5c..37584fe 100644 --- a/services/src/main/java/de/votian/services/entity/GroupwareAppointment.java +++ b/services/src/main/java/de/votian/services/entity/GroupwareAppointment.java @@ -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 diff --git a/services/src/main/java/de/votian/services/entity/Job.java b/services/src/main/java/de/votian/services/entity/Job.java index f333ae3..7b64c27 100644 --- a/services/src/main/java/de/votian/services/entity/Job.java +++ b/services/src/main/java/de/votian/services/entity/Job.java @@ -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") diff --git a/services/src/main/java/de/votian/services/entity/LegacyBlankableIntegerStringConverter.java b/services/src/main/java/de/votian/services/entity/LegacyBlankableIntegerStringConverter.java new file mode 100644 index 0000000..41235f0 --- /dev/null +++ b/services/src/main/java/de/votian/services/entity/LegacyBlankableIntegerStringConverter.java @@ -0,0 +1,25 @@ +package de.votian.services.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class LegacyBlankableIntegerStringConverter implements AttributeConverter { + + @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); + } +} diff --git a/services/src/main/java/de/votian/services/repository/CostCenterRepository.java b/services/src/main/java/de/votian/services/repository/CostCenterRepository.java index 1a41076..d281cd0 100644 --- a/services/src/main/java/de/votian/services/repository/CostCenterRepository.java +++ b/services/src/main/java/de/votian/services/repository/CostCenterRepository.java @@ -18,6 +18,15 @@ public interface CostCenterRepository extends JpaRepository { @Query("SELECT csc FROM CostCenter csc WHERE csc.customerId = :csId AND csc.visible = '1'") List 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 findVisibleNonExternByHeadquartersId(@Param("hqId") Long hqId); + @Query("SELECT csc FROM CostCenter csc WHERE csc.path LIKE %:pathFragment% AND csc.customerId = :csId") List findByPathContainingAndCustomerId(@Param("pathFragment") String pathFragment, @Param("csId") Long csId); } diff --git a/services/src/main/java/de/votian/services/repository/JobRepository.java b/services/src/main/java/de/votian/services/repository/JobRepository.java index e494d17..48c8f62 100644 --- a/services/src/main/java/de/votian/services/repository/JobRepository.java +++ b/services/src/main/java/de/votian/services/repository/JobRepository.java @@ -35,6 +35,11 @@ public interface JobRepository extends JpaRepository { @Query("SELECT j FROM Job j WHERE j.orderTime >= CURRENT_DATE") List 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 findPermanentCopies(@Param("since") LocalDateTime since, @Param("cscIds") List cscIds); diff --git a/services/src/main/java/de/votian/services/repository/UserRepository.java b/services/src/main/java/de/votian/services/repository/UserRepository.java index 84e01e8..27de109 100644 --- a/services/src/main/java/de/votian/services/repository/UserRepository.java +++ b/services/src/main/java/de/votian/services/repository/UserRepository.java @@ -36,10 +36,11 @@ public interface UserRepository extends JpaRepository { List 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 findBirthdaysToday(); @Modifying diff --git a/services/src/main/java/de/votian/services/security/AuthSessionService.java b/services/src/main/java/de/votian/services/security/AuthSessionService.java index 434e2cb..e42c6d2 100644 --- a/services/src/main/java/de/votian/services/security/AuthSessionService.java +++ b/services/src/main/java/de/votian/services/security/AuthSessionService.java @@ -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 int removeExpiredEntries(Map sessions, Instant now) { + int removed = 0; + for (Map.Entry 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 { } } diff --git a/services/src/main/java/de/votian/services/service/AcceptanceProtocolAutomationService.java b/services/src/main/java/de/votian/services/service/AcceptanceProtocolAutomationService.java new file mode 100644 index 0000000..c8dcbda --- /dev/null +++ b/services/src/main/java/de/votian/services/service/AcceptanceProtocolAutomationService.java @@ -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 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 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 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 resolvePayerMailAddress(Long payerCostCenterId) { + if (payerCostCenterId == null) { + return Optional.empty(); + } + + for (int addressTypeId : List.of(4, 2, 3, 1)) { + Optional 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 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 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 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 splitRecipients(String rawRecipients) { + LinkedHashSet 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 to, List cc, List bcc) { + } +} diff --git a/services/src/main/java/de/votian/services/service/JobService.java b/services/src/main/java/de/votian/services/service/JobService.java index e23ff28..16294a5 100644 --- a/services/src/main/java/de/votian/services/service/JobService.java +++ b/services/src/main/java/de/votian/services/service/JobService.java @@ -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 tours, List articles) { return createJob(job, tours, articles, List.of()); diff --git a/services/src/main/java/de/votian/services/service/LegacyCourierAutoLogoutService.java b/services/src/main/java/de/votian/services/service/LegacyCourierAutoLogoutService.java new file mode 100644 index 0000000..25dc480 --- /dev/null +++ b/services/src/main/java/de/votian/services/service/LegacyCourierAutoLogoutService.java @@ -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 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 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) { + } +} diff --git a/services/src/main/java/de/votian/services/service/LegacyFileTransferService.java b/services/src/main/java/de/votian/services/service/LegacyFileTransferService.java new file mode 100644 index 0000000..69e0652 --- /dev/null +++ b/services/src/main/java/de/votian/services/service/LegacyFileTransferService.java @@ -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) { + } +} diff --git a/services/src/main/java/de/votian/services/service/LegacyOperationsScheduler.java b/services/src/main/java/de/votian/services/service/LegacyOperationsScheduler.java new file mode 100644 index 0000000..e6841ee --- /dev/null +++ b/services/src/main/java/de/votian/services/service/LegacyOperationsScheduler.java @@ -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(); + } +} diff --git a/services/src/main/java/de/votian/services/service/LonghaulRemoteDbService.java b/services/src/main/java/de/votian/services/service/LonghaulRemoteDbService.java index 6436853..9d9a8f0 100644 --- a/services/src/main/java/de/votian/services/service/LonghaulRemoteDbService.java +++ b/services/src/main/java/de/votian/services/service/LonghaulRemoteDbService.java @@ -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()); } diff --git a/services/src/main/java/de/votian/services/service/ViewDataService.java b/services/src/main/java/de/votian/services/service/ViewDataService.java index 06e31c8..ed9564d 100644 --- a/services/src/main/java/de/votian/services/service/ViewDataService.java +++ b/services/src/main/java/de/votian/services/service/ViewDataService.java @@ -113,15 +113,11 @@ public class ViewDataService { } public List findCustomersByHeadquarters(Long hqId) { - return customerRepository.findByHeadquartersId(hqId).stream() - .map(this::toCustomerListItem) - .toList(); + return toCustomerListItems(customerRepository.findByHeadquartersId(hqId)); } public List searchCustomers(Long hqId, String term) { - return customerRepository.searchCustomers(hqId, term).stream() - .map(this::toCustomerListItem) - .toList(); + return toCustomerListItems(customerRepository.searchCustomers(hqId, term)); } public Optional findCustomerDetail(Long id) { @@ -129,15 +125,11 @@ public class ViewDataService { } public List findCouriersByHeadquarters(Long hqId) { - return courierRepository.findByHeadquartersId(hqId).stream() - .map(this::toCourierListItem) - .toList(); + return toCourierListItems(courierRepository.findByHeadquartersId(hqId)); } public List searchCouriers(Long hqId, String term) { - return courierRepository.searchCouriers(hqId, term).stream() - .map(this::toCourierListItem) - .toList(); + return toCourierListItems(courierRepository.searchCouriers(hqId, term)); } public Optional findCourierDetail(Long id) { @@ -475,10 +467,7 @@ public class ViewDataService { } public List findJobFilterCostCenters(Long hqId, List accessibleCostCenterIds) { - List costCenters = new ArrayList<>(); - for (Customer customer : customerRepository.findByHeadquartersId(hqId)) { - costCenters.addAll(findVisibleCostCentersSorted(customer.getId())); - } + List costCenters = new ArrayList<>(costCenterRepository.findVisibleNonExternByHeadquartersId(hqId)); if (accessibleCostCenterIds != null) { costCenters = filterCostCentersByAccess(costCenters, accessibleCostCenterIds); } @@ -525,6 +514,51 @@ public class ViewDataService { return dto; } + private List toCustomerListItems(List customers) { + Map 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 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 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 toCourierListItems(List couriers) { + Map 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 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()); diff --git a/services/src/main/resources/application.properties b/services/src/main/resources/application.properties index 4799cf3..fb4acc5 100644 --- a/services/src/main/resources/application.properties +++ b/services/src/main/resources/application.properties @@ -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 diff --git a/vaadin/src/main/java/de/votian/web/view/DashboardView.java b/vaadin/src/main/java/de/votian/web/view/DashboardView.java index 661f5f1..eee385d 100644 --- a/vaadin/src/main/java/de/votian/web/view/DashboardView.java +++ b/vaadin/src/main/java/de/votian/web/view/DashboardView.java @@ -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 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")); }