Rufnummern: Vorwahl als eigenes Feld – verlässliche PDF-Befüllung
Bisher steht in PhoneNumber.phoneNumber die kombinierte Nummer
("04264 836975"). Die Wechselauftrag-PDFs splittten heuristisch
auf Vorwahl/Anschluss, was bei Sonderformaten daneben ging.
Schema: PhoneNumber.areaCode String? (optional, Bestandsdaten
werden beim nächsten Edit nachgepflegt). Migration
20260601200000_phone_area_code mit IF NOT EXISTS.
ContractForm: aus "Rufnummer" werden zwei Felder – "Vorwahl" und
"Rufnummer". Beim Speichern sendet das Frontend areaCode separat
UND die kombinierte phoneNumber (für Listen/Suchen weiter
unverändert). Beim Edit-Load wird areaCode bevorzugt; falls leer,
splittet die UI heuristisch und prefillt beides – User kann
korrigieren und beim Speichern wird der saubere Wert persistiert.
PDF-Template-Service: phoneAreaCode[N] und phoneLocal[N]
verwenden jetzt primär den gespeicherten areaCode aus der DB
(verlässlich), Heuristik nur als Fallback für Altbestand. Die
Template-Variablen-Liste war bereits korrekt definiert, jetzt
ist die Datenquelle solide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
-- Telefonnummern bekommen eine zusätzliche Spalte `areaCode`, damit
|
||||
-- Wechselauftrag-PDFs die Vorwahl verlässlich befüllen können, ohne
|
||||
-- den heuristischen String-Split, der bei Sonderformaten danebenfasst.
|
||||
--
|
||||
-- `phoneNumber` bleibt unverändert die komplette Nummer (Vorwahl +
|
||||
-- Anschlussnummer), damit Reports/Listen/Suchen weiter funktionieren.
|
||||
-- `areaCode` ist optional, weil Bestandsnummern erst beim nächsten
|
||||
-- Edit nachgepflegt werden.
|
||||
--
|
||||
-- IF NOT EXISTS macht den Re-Deploy auf Prod sicher, falls jemand
|
||||
-- schon `prisma db push` gefahren hat.
|
||||
|
||||
ALTER TABLE `PhoneNumber`
|
||||
ADD COLUMN IF NOT EXISTS `areaCode` VARCHAR(191) NULL;
|
||||
@@ -892,7 +892,12 @@ model PhoneNumber {
|
||||
id Int @id @default(autoincrement())
|
||||
internetContractDetailsId Int
|
||||
internetDetails InternetContractDetails @relation(fields: [internetContractDetailsId], references: [id], onDelete: Cascade)
|
||||
// phoneNumber bleibt die komplette Nummer (Vorwahl + Anschluss) für
|
||||
// bestehende Reports/Listen. Vorwahl wird zusätzlich getrennt erfasst,
|
||||
// damit Auftragsformulare (Wechselauftrag PDF) sie verlässlich
|
||||
// befüllen können – ohne heuristischen String-Split.
|
||||
phoneNumber String
|
||||
areaCode String?
|
||||
isMain Boolean @default(false)
|
||||
// SIP-Zugangsdaten
|
||||
sipUsername String?
|
||||
|
||||
@@ -250,6 +250,7 @@ interface ContractCreateData {
|
||||
phoneNumbers?: {
|
||||
id?: number;
|
||||
phoneNumber: string;
|
||||
areaCode?: string;
|
||||
isMain?: boolean;
|
||||
sipUsername?: string;
|
||||
sipPassword?: string;
|
||||
@@ -345,6 +346,7 @@ export async function createContract(data: ContractCreateData) {
|
||||
? {
|
||||
create: internetDetails.phoneNumbers.map((pn) => ({
|
||||
phoneNumber: pn.phoneNumber,
|
||||
areaCode: pn.areaCode,
|
||||
isMain: pn.isMain ?? false,
|
||||
sipUsername: pn.sipUsername,
|
||||
sipPasswordEncrypted: pn.sipPassword
|
||||
@@ -542,6 +544,7 @@ export async function updateContract(
|
||||
return {
|
||||
internetContractDetailsId: existing.id,
|
||||
phoneNumber: pn.phoneNumber,
|
||||
areaCode: pn.areaCode,
|
||||
isMain: pn.isMain ?? false,
|
||||
sipUsername: pn.sipUsername,
|
||||
// Preserve existing sipPassword if no new value provided
|
||||
@@ -565,6 +568,7 @@ export async function updateContract(
|
||||
? {
|
||||
create: phoneNumbers.map((pn) => ({
|
||||
phoneNumber: pn.phoneNumber,
|
||||
areaCode: pn.areaCode,
|
||||
isMain: pn.isMain ?? false,
|
||||
sipUsername: pn.sipUsername,
|
||||
sipPasswordEncrypted: pn.sipPassword
|
||||
|
||||
@@ -561,8 +561,31 @@ export async function generateFilledPdf(
|
||||
|
||||
const maxFields = template.maxPhoneFields || 8;
|
||||
for (let i = 0; i < Math.max(maxFields, phoneNumbers.length); i++) {
|
||||
const fullNumber = phoneNumbers[i]?.phoneNumber || '';
|
||||
const { areaCode, local } = splitPhoneNumber(fullNumber);
|
||||
const entry = phoneNumbers[i];
|
||||
const fullNumber = entry?.phoneNumber || '';
|
||||
// Bevorzugt den explizit gepflegten areaCode aus der DB (verlässlich),
|
||||
// fällt sonst auf die Heuristik zurück (Altbestand ohne separates
|
||||
// Vorwahl-Feld). `phoneLocal` analog: aus phoneNumber abgeleitet,
|
||||
// wenn areaCode da → den Vorwahl-Prefix abschneiden, sonst Heuristik.
|
||||
let areaCode = '';
|
||||
let local = '';
|
||||
if (entry?.areaCode) {
|
||||
areaCode = entry.areaCode;
|
||||
const split = splitPhoneNumber(fullNumber);
|
||||
// Wenn der heuristische areaCode mit dem DB-Wert übereinstimmt,
|
||||
// ist der heuristische local-Anteil korrekt – sonst pragmatisch:
|
||||
// alles nach dem areaCode-Prefix bis zum Ende
|
||||
if (split.areaCode === entry.areaCode) {
|
||||
local = split.local;
|
||||
} else {
|
||||
const stripped = fullNumber.replace(new RegExp('^' + entry.areaCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\s\\-/]*'), '').trim();
|
||||
local = stripped || split.local;
|
||||
}
|
||||
} else {
|
||||
const split = splitPhoneNumber(fullNumber);
|
||||
areaCode = split.areaCode;
|
||||
local = split.local;
|
||||
}
|
||||
dataContext[`phoneNumbers[${i}]`] = fullNumber;
|
||||
dataContext[`phoneAreaCode[${i}]`] = areaCode;
|
||||
dataContext[`phoneLocal[${i}]`] = local;
|
||||
|
||||
Reference in New Issue
Block a user