diff --git a/crates/application/src/dto/tour_sync.rs b/crates/application/src/dto/tour_sync.rs index 20a03d2..3aecb82 100644 --- a/crates/application/src/dto/tour_sync.rs +++ b/crates/application/src/dto/tour_sync.rs @@ -96,6 +96,9 @@ pub struct SyncContactSource { pub funktion: Option, #[serde(default)] pub channels: Vec, + /// Anschrift dieses Adress-Datensatzes, falls gepflegt. + #[serde(default)] + pub address: Option
, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/crates/domain/src/contact.rs b/crates/domain/src/contact.rs index cad5d22..09988e0 100644 --- a/crates/domain/src/contact.rs +++ b/crates/domain/src/contact.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::Address; + /// Rolle, mit der ein Kontakt-Datensatz an einer Lieferung hängt. Spiegelt /// die fünf Adress-FKs von `Belegkopf` (bzw. den Umweg über den Kunden): /// `header` = Belegadresse, `delivery` = Lieferadresse, `billing` = @@ -30,10 +32,9 @@ pub enum ContactKind { } /// Snapshot eines ERP-Adress-Datensatzes, der zum Zeitpunkt des Tour-Syncs -/// an einer Lieferung hing — Namensblock ohne Anschrift, weil die Adresse -/// ihrerseits schon im Lieferungs-Snapshot steckt (`snap_*`-Spalten). Die -/// eigentlichen Telefonnummern, E-Mails etc. liegen in den -/// zugehörigen [`ContactChannel`]s. +/// an einer Lieferung hing — Namensblock plus (falls im ERP gepflegt) die +/// Anschrift dieses Datensatzes. Die eigentlichen Telefonnummern, E-Mails +/// etc. liegen in den zugehörigen [`ContactChannel`]s. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[serde(rename_all = "camelCase")] @@ -48,6 +49,10 @@ pub struct ContactSource { pub name3: Option, pub abteilung: Option, pub funktion: Option, + /// Anschrift dieses Adress-Datensatzes; `None`, wenn im ERP weder + /// Straße noch Ort gepflegt sind (z. B. reiner Ansprechpartner). + #[serde(default)] + pub address: Option
, } /// Ein einzelner Kontaktkanal (Telefonnummer / Mobil / E-Mail / Web). diff --git a/crates/infrastructure/src/erp/mssql_delivery_source.rs b/crates/infrastructure/src/erp/mssql_delivery_source.rs index 24754f1..096fc82 100644 --- a/crates/infrastructure/src/erp/mssql_delivery_source.rs +++ b/crates/infrastructure/src/erp/mssql_delivery_source.rs @@ -86,15 +86,22 @@ const ADDRESS_CONTACT_COLUMNS: &[&str] = &[ "Telefon3", "Telefon4", "Mobiltel", "Mobiltel2", "EMail", "EMail2", "EMail3", "InternetAdresse", ]; +/// Anschrift-Spalten je Adresse (`Land` = ERP-Freitext, praktisch Zusatz). +const ADDRESS_POSTAL_COLUMNS: &[&str] = &["Strasse", "Hausnummer", "PLZ", "Ort", "Land"]; + /// Erzeugt den SELECT-Block für alle 5 Adressen — eine Zeile pro Spalte mit -/// `. AS _`. Wird in beide UNION-Hälften identisch -/// einkopiert. +/// `. AS _`, dazu das Land aus dem Länderstamm als +/// `_LandName`. Wird in beide UNION-Hälften identisch einkopiert. fn address_select_block() -> String { let mut out = String::new(); for (alias, prefix) in ADDRESS_ALIASES { - for col in ADDRESS_CONTACT_COLUMNS { + for col in ADDRESS_CONTACT_COLUMNS.iter().chain(ADDRESS_POSTAL_COLUMNS) { out.push_str(&format!(" {alias}.{col} AS {prefix}_{col},\n")); } + out.push_str(&format!( + " (SELECT LTRIM(RTRIM(l.Land)) FROM Laender l WHERE l.ROW_ID = {alias}.LandID) \ + AS {prefix}_LandName,\n" + )); } out } @@ -348,8 +355,9 @@ fn boolc(row: &tiberius::Row, col: &str) -> bool { /// Liest aus einem Tiberius-Row die Kontaktdaten **einer** Adress-Rolle aus /// (alle Spalten heißen `_`, siehe [`address_select_block`]). -/// Gibt `None` zurück, wenn weder ein Namensfeld noch ein Kontaktkanal -/// belegt ist — leere Adressen werden so vom Sync nicht mitgeschleppt. +/// Gibt `None` zurück, wenn weder ein Namensfeld noch ein Kontaktkanal noch +/// eine Anschrift belegt ist — leere Adressen werden so vom Sync nicht +/// mitgeschleppt. fn read_contact_source( row: &tiberius::Row, prefix: &str, @@ -391,6 +399,21 @@ fn read_contact_source( push(&mut channels, ContactKind::Email, 3, get("EMail3")); push(&mut channels, ContactKind::Web, 1, get("InternetAdresse")); + // Anschrift nur, wenn Straße oder Ort gepflegt sind. + let street = get("Strasse"); + let city = get("Ort"); + let address = (street.is_some() || city.is_some()).then(|| { + let country = get("LandName").unwrap_or_default(); + Address { + street: street.unwrap_or_default(), + house_number: get("Hausnummer").unwrap_or_default(), + postal_code: get("PLZ").unwrap_or_default(), + city: city.unwrap_or_default(), + addition: address_addition(get("Land"), &country), + country, + } + }); + let has_name = anrede.is_some() || titel.is_some() || name1.is_some() @@ -398,7 +421,7 @@ fn read_contact_source( || name3.is_some() || abteilung.is_some() || funktion.is_some(); - if channels.is_empty() && !has_name { + if channels.is_empty() && !has_name && address.is_none() { return None; } @@ -412,6 +435,7 @@ fn read_contact_source( abteilung, funktion, channels, + address, }) } diff --git a/crates/infrastructure/src/persistence/tour_repository.rs b/crates/infrastructure/src/persistence/tour_repository.rs index 2242fbf..4d036a1 100644 --- a/crates/infrastructure/src/persistence/tour_repository.rs +++ b/crates/infrastructure/src/persistence/tour_repository.rs @@ -161,6 +161,12 @@ struct ContactSourceRow { name3: Option, abteilung: Option, funktion: Option, + street: Option, + house_number: Option, + postal_code: Option, + city: Option, + country: Option, + address_addition: Option, } #[derive(sqlx::FromRow)] @@ -280,6 +286,19 @@ fn map_contact_source(row: ContactSourceRow) -> Result( r#" SELECT id, delivery_id, role, - anrede, titel, name1, name2, name3, abteilung, funktion + anrede, titel, name1, name2, name3, abteilung, funktion, + street, house_number, postal_code, city, country, address_addition FROM delivery_contact_sources WHERE delivery_id = ANY($1) ORDER BY delivery_id, role @@ -1319,8 +1339,9 @@ async fn replace_contact_sources( r#" INSERT INTO delivery_contact_sources ( delivery_id, role, anrede, titel, name1, name2, name3, - abteilung, funktion - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + abteilung, funktion, + street, house_number, postal_code, city, country, address_addition + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id "#, ) @@ -1333,6 +1354,12 @@ async fn replace_contact_sources( .bind(src.name3.as_deref()) .bind(src.abteilung.as_deref()) .bind(src.funktion.as_deref()) + .bind(src.address.as_ref().map(|a| a.street.as_str())) + .bind(src.address.as_ref().map(|a| a.house_number.as_str())) + .bind(src.address.as_ref().map(|a| a.postal_code.as_str())) + .bind(src.address.as_ref().map(|a| a.city.as_str())) + .bind(src.address.as_ref().map(|a| a.country.as_str())) + .bind(src.address.as_ref().and_then(|a| a.addition.as_deref())) .fetch_one(&mut **tx) .await .map_err(db)?; diff --git a/migrations/0035_contact_source_address.sql b/migrations/0035_contact_source_address.sql new file mode 100644 index 0000000..6c4db8a --- /dev/null +++ b/migrations/0035_contact_source_address.sql @@ -0,0 +1,14 @@ +-- Anschrift je Kontaktquelle (Beleg-, Liefer-, Rechnungsadresse, +-- Ansprechpartner, Kundenstamm). Bisher trug eine Quelle nur Namensblock +-- und Kanäle; die App zeigt unter „Alle Kontaktinfos" jetzt auch die +-- Anschrift, sofern im ERP gepflegt. Alles NULL = keine Anschrift. +-- Felder wie bei `customers` / `deliveries.snap_*`: `country` aus dem +-- ERP-Länderstamm, `address_addition` = ERP-Freitext `Adressen.Land`. + +ALTER TABLE delivery_contact_sources + ADD COLUMN street TEXT, + ADD COLUMN house_number TEXT, + ADD COLUMN postal_code TEXT, + ADD COLUMN city TEXT, + ADD COLUMN country TEXT, + ADD COLUMN address_addition TEXT;