SIM-Karten: Feld "Kartennutzer" für Firmen-/Familienverträge

Bei Firmenverträgen (Vertragsinhaber = Firma, Nutzer = Mitarbeiter)
und Familienverträgen (Inhaber = Eltern, Nutzer = Kind) brauchten
wir ein Feld, das den tatsächlichen Nutzer der SIM-Karte erfasst.

Backend: SimCard.cardUser (String?, optional), Migration
20260601100000_sim_card_user mit IF NOT EXISTS. Im Service durch
Create + Update propagiert.

Frontend: Input "Kartennutzer" pro SIM-Karte in ContractForm
(eigene Zeile oberhalb der technischen Felder Rufnummer/SIM-Nr/
PIN/PUK). In ContractDetail wird der Nutzer als "Nutzer: <Name>"
neben den Hauptkarte/Multisim-Badges angezeigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 08:10:16 +02:00
parent 9e3bce85f0
commit 4acfd9de1c
6 changed files with 42 additions and 1 deletions
@@ -0,0 +1,9 @@
-- SIM-Karte bekommt einen optionalen "Kartennutzer" relevant bei Firmen-
-- und Familienverträgen, wo der Vertragsinhaber (Firma/Eltern) nicht
-- gleich dem tatsächlichen Nutzer (Mitarbeiter/Kind) ist.
--
-- IF NOT EXISTS macht den Re-Deploy auf Prod sicher, falls jemand schon
-- `prisma db push` gefahren hat.
ALTER TABLE `SimCard`
ADD COLUMN IF NOT EXISTS `cardUser` VARCHAR(191) NULL;
+3
View File
@@ -928,6 +928,9 @@ model SimCard {
puk String? // PUK (verschlüsselt gespeichert) puk String? // PUK (verschlüsselt gespeichert)
isMultisim Boolean @default(false) // Ist dies eine Multisim-Karte? isMultisim Boolean @default(false) // Ist dies eine Multisim-Karte?
isMain Boolean @default(false) // Ist dies die Hauptkarte? isMain Boolean @default(false) // Ist dies die Hauptkarte?
// Tatsächlicher Nutzer der SIM-Karte (z.B. Mitarbeiter bei Firmenverträgen,
// Kind bei Eltern-Vertrag) kann vom Vertragsinhaber abweichen.
cardUser String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
+4
View File
@@ -275,6 +275,7 @@ interface ContractCreateData {
puk?: string; puk?: string;
isMultisim?: boolean; isMultisim?: boolean;
isMain?: boolean; isMain?: boolean;
cardUser?: string;
}[]; }[];
}; };
tvDetails?: { tvDetails?: {
@@ -378,6 +379,7 @@ export async function createContract(data: ContractCreateData) {
puk: sc.puk ? encrypt(sc.puk) : undefined, puk: sc.puk ? encrypt(sc.puk) : undefined,
isMultisim: sc.isMultisim ?? false, isMultisim: sc.isMultisim ?? false,
isMain: sc.isMain ?? false, isMain: sc.isMain ?? false,
cardUser: sc.cardUser,
})), })),
} }
: undefined, : undefined,
@@ -614,6 +616,7 @@ export async function updateContract(
puk: sc.puk ? encrypt(sc.puk) : (existingSc?.puk ?? undefined), puk: sc.puk ? encrypt(sc.puk) : (existingSc?.puk ?? undefined),
isMultisim: sc.isMultisim ?? false, isMultisim: sc.isMultisim ?? false,
isMain: sc.isMain ?? false, isMain: sc.isMain ?? false,
cardUser: sc.cardUser,
}; };
}), }),
}); });
@@ -632,6 +635,7 @@ export async function updateContract(
puk: sc.puk ? encrypt(sc.puk) : undefined, puk: sc.puk ? encrypt(sc.puk) : undefined,
isMultisim: sc.isMultisim ?? false, isMultisim: sc.isMultisim ?? false,
isMain: sc.isMain ?? false, isMain: sc.isMain ?? false,
cardUser: sc.cardUser,
})), })),
} }
: undefined, : undefined,
@@ -134,9 +134,15 @@ function SimCardDisplay({ simCard }: { simCard: SimCard }) {
return ( return (
<div className="p-3 bg-gray-50 rounded-lg border"> <div className="p-3 bg-gray-50 rounded-lg border">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2 flex-wrap">
{simCard.isMain && <Badge variant="success">Hauptkarte</Badge>} {simCard.isMain && <Badge variant="success">Hauptkarte</Badge>}
{simCard.isMultisim && <Badge variant="warning">Multisim</Badge>} {simCard.isMultisim && <Badge variant="warning">Multisim</Badge>}
{simCard.cardUser && (
<span className="text-sm text-gray-700">
<span className="text-gray-500">Nutzer:</span>{' '}
<span className="font-medium">{simCard.cardUser}</span>
</span>
)}
</div> </div>
<dl className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm"> <dl className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
{simCard.phoneNumber && ( {simCard.phoneNumber && (
@@ -192,6 +192,7 @@ export default function ContractForm() {
hasExistingPuk?: boolean; // Zeigt an ob PUK bereits in DB vorhanden hasExistingPuk?: boolean; // Zeigt an ob PUK bereits in DB vorhanden
isMultisim: boolean; isMultisim: boolean;
isMain: boolean; isMain: boolean;
cardUser: string;
} }
const [simCards, setSimCards] = useState<SimCardInput[]>([]); const [simCards, setSimCards] = useState<SimCardInput[]>([]);
@@ -379,6 +380,7 @@ export default function ContractForm() {
hasExistingPuk: !!sc.puk, // true wenn verschlüsselter Wert vorhanden hasExistingPuk: !!sc.puk, // true wenn verschlüsselter Wert vorhanden
isMultisim: sc.isMultisim, isMultisim: sc.isMultisim,
isMain: sc.isMain, isMain: sc.isMain,
cardUser: sc.cardUser || '',
}))); })));
} else { } else {
setSimCards([]); setSimCards([]);
@@ -610,6 +612,7 @@ export default function ContractForm() {
puk: sc.puk || undefined, // Passwort: undefined = nicht ändern puk: sc.puk || undefined, // Passwort: undefined = nicht ändern
isMultisim: sc.isMultisim, isMultisim: sc.isMultisim,
isMain: sc.isMain, isMain: sc.isMain,
cardUser: emptyToNull(sc.cardUser),
})) : undefined, })) : undefined,
}; };
} }
@@ -1490,6 +1493,18 @@ export default function ContractForm() {
<Trash2 className="w-4 h-4 text-red-500" /> <Trash2 className="w-4 h-4 text-red-500" />
</Button> </Button>
</div> </div>
<div className="mb-3">
<Input
label="Kartennutzer"
value={card.cardUser}
onChange={(e) => {
const updated = [...simCards];
updated[index].cardUser = e.target.value;
setSimCards(updated);
}}
placeholder="z.B. Mitarbeiter-/Kind-Name (optional, kann vom Vertragsinhaber abweichen)"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
<Input <Input
label="Rufnummer" label="Rufnummer"
@@ -1578,6 +1593,7 @@ export default function ContractForm() {
puk: '', puk: '',
isMultisim: false, isMultisim: false,
isMain: simCards.length === 0, // Erste Karte ist Hauptkarte isMain: simCards.length === 0, // Erste Karte ist Hauptkarte
cardUser: '',
}]); }]);
}} }}
> >
+3
View File
@@ -557,6 +557,9 @@ export interface SimCard {
puk?: string; // verschlüsselt puk?: string; // verschlüsselt
isMultisim: boolean; isMultisim: boolean;
isMain: boolean; isMain: boolean;
// Tatsächlicher Nutzer der Karte (z.B. Mitarbeiter bei Firmenvertrag,
// Kind bei Eltern-Vertrag) optional, kann vom Vertragsinhaber abweichen.
cardUser?: string;
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
} }