Compare commits

..
2 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 4.7 8eb3790e7b Mobilfunk: Feld "Mobilfunknetz" unter Anbieter & Tarif
Neues Dropdown "Mobilfunknetz" in der Anbieter-&-Tarif-Karte, nur
sichtbar bei Vertragstyp Mobilfunk. Optionen: Bitte auswählen (leer),
Telekom, Vodafone, Telefónica.

Neues Feld MobileContractDetails.mobileNetwork (String nullable,
speichert TELEKOM/VODAFONE/TELEFONICA) + idempotente Migration
(ADD COLUMN IF NOT EXISTS). String statt Enum, damit weitere Netze
ohne Migration ergänzbar sind. Anzeige in der Vertragsansicht mit
lesbarem Netz-Namen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 13:07:56 +02:00
duffyduckandClaude Opus 4.7 8508bdc38e Lieferbestätigung: eingegebenes Datum überschreibt Vertragsbeginn
Beim Upload einer Lieferbestätigung mit Datum wurde startDate nur
gesetzt, wenn es noch leer war (if (!contract.startDate)). Hatte der
Vertrag schon ein geschätztes Beginndatum, blieb es trotz
eingetragenem Lieferdatum stehen.

Fix in maybeActivateOnDeliveryConfirmation: ein explizit eingegebenes
Lieferdatum übernimmt jetzt IMMER den Vertragsbeginn (die Liefer-
bestätigung ist das maßgebliche tatsächliche Startdatum), auch
überschreibend. Der Fallback "heute" (kein Datum angegeben) füllt
weiterhin nur ein leeres Feld, damit ein echtes Datum nicht
versehentlich mit heute überschrieben wird. No-op-Guard + Audit-Log
unverändert.

Frontend schickte das Datum bereits mit und lädt den Vertrag nach
dem Upload neu – kein Frontend-Change nötig.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 12:49:55 +02:00
8 changed files with 82 additions and 6 deletions
@@ -0,0 +1,6 @@
-- Mobilfunknetz pro Mobilfunkvertrag (TELEKOM | VODAFONE | TELEFONICA,
-- NULL = nicht gewählt). String statt Enum, damit weitere Netze ohne
-- Migration ergänzbar sind.
ALTER TABLE `MobileContractDetails`
ADD COLUMN IF NOT EXISTS `mobileNetwork` VARCHAR(191) NULL;
+4
View File
@@ -970,6 +970,10 @@ model MobileContractDetails {
contractId Int @unique
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
requiresMultisim Boolean @default(false) // Multisim erforderlich?
// Physisches Mobilfunknetz, auf dem der Tarif läuft: TELEKOM | VODAFONE
// | TELEFONICA (null = nicht gewählt). Als String statt Enum gehalten,
// damit weitere Netze ohne Migration ergänzbar sind.
mobileNetwork String?
dataVolume Float?
includedMinutes Int?
includedSMS Int?
+2
View File
@@ -279,6 +279,7 @@ interface ContractCreateData {
};
mobileDetails?: {
requiresMultisim?: boolean;
mobileNetwork?: string | null;
dataVolume?: number;
includedMinutes?: number;
includedSMS?: number;
@@ -386,6 +387,7 @@ export async function createContract(data: ContractCreateData) {
mobileDetails: {
create: {
requiresMultisim: mobileDetails.requiresMultisim,
mobileNetwork: mobileDetails.mobileNetwork ?? null,
dataVolume: mobileDetails.dataVolume,
includedMinutes: mobileDetails.includedMinutes,
includedSMS: mobileDetails.includedSMS,
@@ -144,7 +144,14 @@ export async function withContractDocumentLock<T>(
* Wird nach einem ContractDocument-Upload aufgerufen. Wenn der Typ eine
* Lieferbestätigung ist:
* - Contract.status von DRAFT auf ACTIVE setzen (falls DRAFT)
* - Contract.startDate auf deliveryDate (oder heute) setzen, falls noch leer
* - Contract.startDate auf das Lieferdatum setzen:
* * Explizit eingegebenes deliveryDate → IMMER als Vertragsbeginn
* übernehmen, auch wenn schon ein Datum gesetzt war. Die
* Lieferbestätigung ist das maßgebliche tatsächliche Startdatum
* und korrigiert ein evtl. vorher geschätztes Beginndatum.
* * Kein deliveryDate angegeben → Fallback "heute", aber NUR wenn
* startDate noch leer ist. Ein bestehendes (echtes) Datum darf
* nicht versehentlich mit "heute" überschrieben werden.
*
* Schreibweise "Lieferbestätigung" stammt aus dem Frontend-Dropdown
* (SaveAttachmentModal / ContractDetail). Vergleich case-insensitive +
@@ -165,13 +172,12 @@ export async function maybeActivateOnDeliveryConfirmation(
});
if (!contract) return;
// deliveryDate parsen, Fallback auf heute
// Explizit eingegebenes Lieferdatum parsen (null = keins angegeben).
let parsedDate: Date | null = null;
if (deliveryDate) {
const parsed = new Date(deliveryDate);
if (!isNaN(parsed.getTime())) parsedDate = parsed;
}
const effectiveDate = parsedDate || new Date();
const updateData: Record<string, unknown> = {};
const changes: Record<string, { vorher: unknown; nachher: unknown }> = {};
@@ -181,9 +187,21 @@ export async function maybeActivateOnDeliveryConfirmation(
changes.status = { vorher: 'DRAFT', nachher: 'ACTIVE' };
}
if (!contract.startDate) {
updateData.startDate = effectiveDate;
changes.startDate = { vorher: null, nachher: effectiveDate.toISOString().split('T')[0] };
const asDay = (d: Date | null | undefined) =>
d ? new Date(d).toISOString().split('T')[0] : null;
if (parsedDate) {
// Explizites Lieferdatum: als Vertragsbeginn übernehmen, auch überschreibend.
// No-op vermeiden, wenn der Tag schon exakt passt.
if (asDay(contract.startDate) !== asDay(parsedDate)) {
updateData.startDate = parsedDate;
changes.startDate = { vorher: asDay(contract.startDate), nachher: asDay(parsedDate) };
}
} else if (!contract.startDate) {
// Kein Datum angegeben → Fallback heute, nur bei leerem Startdatum.
const today = new Date();
updateData.startDate = today;
changes.startDate = { vorher: null, nachher: asDay(today) };
}
if (Object.keys(updateData).length === 0) return;
+24
View File
@@ -97,6 +97,30 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **📱 Mobilfunk: Feld „Mobilfunknetz" unter Anbieter & Tarif**
- Neues Dropdown „Mobilfunknetz" in der Anbieter-&-Tarif-Karte, nur
sichtbar wenn Vertragstyp = Mobilfunk. Optionen: Bitte auswählen
(leer), Telekom, Vodafone, Telefónica.
- Neues Feld `MobileContractDetails.mobileNetwork` (String, nullable,
speichert TELEKOM/VODAFONE/TELEFONICA) + Migration
`20260727100000_mobile_network` (`ADD COLUMN IF NOT EXISTS`).
String statt Enum, damit weitere Netze ohne Migration möglich sind.
- Anzeige in der Vertragsansicht (Mobilfunk-Details) mit lesbarem
Netz-Namen.
- [x] **🐞 Lieferbestätigung: eingegebenes Datum ändert Vertragsbeginn nicht**
- Beim Upload einer Lieferbestätigung mit Datum wurde `startDate` nur
gesetzt, wenn es noch LEER war (`if (!contract.startDate)`). Hatte
der Vertrag schon ein (geschätztes) Beginndatum, blieb es trotz
eingetragenem Lieferdatum stehen.
- Fix in `maybeActivateOnDeliveryConfirmation`: ein explizit
eingegebenes Lieferdatum überschreibt den Vertragsbeginn jetzt IMMER
(die Lieferbestätigung ist das maßgebliche tatsächliche Startdatum).
Der Fallback „heute" (kein Datum eingegeben) füllt weiterhin nur ein
leeres Feld, um ein echtes Datum nicht versehentlich zu überschreiben.
No-op + Audit-Log unverändert. Frontend schickte das Datum bereits
mit und lädt den Vertrag nach Upload neu kein FE-Change nötig.
- [x] **🎂 Anstehende Geburtstage auch im Dashboard**
- Die Geburtstags-Sektion gab es bisher nur im Vertrags-Cockpit.
Jetzt 1:1 auch auf dem Dashboard (nur Mitarbeiter/Admin,
@@ -3077,6 +3077,12 @@ export default function ContractDetail() {
{c.mobileDetails && (
<Card className="mb-6" title="Mobilfunk-Details">
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
{c.mobileDetails.mobileNetwork && (
<div>
<dt className="text-sm text-gray-500">Mobilfunknetz</dt>
<dd>{({ TELEKOM: 'Telekom', VODAFONE: 'Vodafone', TELEFONICA: 'Telefónica' } as Record<string, string>)[c.mobileDetails.mobileNetwork] || c.mobileDetails.mobileNetwork}</dd>
</div>
)}
{c.mobileDetails.dataVolume && (
<div>
<dt className="text-sm text-gray-500">Datenvolumen</dt>
@@ -345,6 +345,7 @@ export default function ContractForm() {
activationCode: c.internetDetails?.activationCode || '',
// Mobile details
requiresMultisim: c.mobileDetails?.requiresMultisim || false,
mobileNetwork: c.mobileDetails?.mobileNetwork || '',
dataVolume: c.mobileDetails?.dataVolume || '',
includedMinutes: c.mobileDetails?.includedMinutes || '',
includedSMS: c.mobileDetails?.includedSMS || '',
@@ -656,6 +657,7 @@ export default function ContractForm() {
if (data.type === 'MOBILE') {
contractData.mobileDetails = {
requiresMultisim: data.requiresMultisim || false,
mobileNetwork: emptyToNull(data.mobileNetwork),
dataVolume: data.dataVolume ? parseFloat(data.dataVolume) : null,
includedMinutes: safeParseInt(data.includedMinutes) ?? null,
includedSMS: safeParseInt(data.includedSMS) ?? null,
@@ -1019,6 +1021,18 @@ export default function ContractForm() {
options={availableTariffs.map((t) => ({ value: t.id, label: t.name }))}
disabled={!selectedProviderId}
/>
{contractType === 'MOBILE' && (
<Select
label="Mobilfunknetz"
{...register('mobileNetwork')}
options={[
{ value: 'TELEKOM', label: 'Telekom' },
{ value: 'VODAFONE', label: 'Vodafone' },
{ value: 'TELEFONICA', label: 'Telefónica' },
]}
placeholder="Bitte auswählen"
/>
)}
<Input label="Kundennummer beim Anbieter" maxLength={100} {...register('customerNumberAtProvider')} />
<Input label="Vertragsnummer beim Anbieter" maxLength={100} {...register('contractNumberAtProvider')} />
<Input label="Auftragsnummer bei Vertriebsplattform" maxLength={100} {...register('orderNumberAtSalesPlatform')} />
+2
View File
@@ -588,6 +588,8 @@ export interface MobileContractDetails {
id: number;
contractId: number;
requiresMultisim?: boolean;
/** Physisches Mobilfunknetz: TELEKOM | VODAFONE | TELEFONICA (oder null). */
mobileNetwork?: string | null;
dataVolume?: number;
includedMinutes?: number;
includedSMS?: number;