Vertreter: Portal-Pflicht raus, Warn-Badge stattdessen

Suche und Add haben bisher nur Kunden mit portalEnabled=true zugelassen –
das machte das Feature quasi unnutzbar, weil die meisten Kunden kein
Portal aktiviert haben. Der Zugriff ist ohnehin erst dann effektiv,
wenn der Vertreter ein Portal-Konto bekommt.

- searchCustomersForRepresentative: portalEnabled-Filter raus, dafür
  Feld im select mitgeliefert.
- addRepresentative: Portal-Pflicht-Check raus.
- getRepresentedByList: portalEnabled im rep-Include, damit die UI
  auch für schon hinterlegte Vertreter das Badge zeigen kann.
- CustomerDetail: gelbes "Portal inaktiv"-Badge in Suchergebnissen
  und in der Vertreter-Liste. Hinweistext geändert.
- CustomerSummary-Type: portalEnabled? ergänzt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 08:53:02 +02:00
co-authored by Claude Opus 4.7
parent 9c5c22d92b
commit 4d3382b5a4
3 changed files with 31 additions and 11 deletions
+16 -8
View File
@@ -900,7 +900,10 @@ export async function getCustomerRepresentatives(customerId: number) {
} }
export async function getRepresentedByList(customerId: number) { export async function getRepresentedByList(customerId: number) {
// Holt alle Kunden, die den angegebenen Kunden vertreten können // Holt alle Kunden, die den angegebenen Kunden vertreten können.
// `portalEnabled` mit rausgeben, damit die UI ein Badge zeigen kann,
// falls der Vertreter noch keinen Portal-Zugang hat (Vertretung ist
// dann formal eingetragen, greift aber erst mit aktivem Portal).
return prisma.customerRepresentative.findMany({ return prisma.customerRepresentative.findMany({
where: { customerId: customerId, isActive: true }, where: { customerId: customerId, isActive: true },
include: { include: {
@@ -912,6 +915,7 @@ export async function getRepresentedByList(customerId: number) {
lastName: true, lastName: true,
companyName: true, companyName: true,
type: true, type: true,
portalEnabled: true,
}, },
}, },
}, },
@@ -940,10 +944,10 @@ export async function addRepresentative(
throw new Error('Ein Kunde kann sich nicht selbst vertreten'); throw new Error('Ein Kunde kann sich nicht selbst vertreten');
} }
// Prüfen ob der Vertreter ein Portal-Konto hat // Portal-Zwang entfernt (2026-07-03): der Vertreter darf ohne Portal
if (!representative.portalEnabled) { // eingetragen werden; die Beziehung ist erst dann effektiv aktiv,
throw new Error('Der Vertreter-Kunde muss ein aktiviertes Portal-Konto haben'); // wenn der Vertreter einen Portal-Zugang bekommt (die Frontend-UI
} // zeigt in dem Fall ein Warn-Badge).
return prisma.customerRepresentative.upsert({ return prisma.customerRepresentative.upsert({
where: { where: {
@@ -985,12 +989,15 @@ export async function removeRepresentative(customerId: number, representativeId:
} }
export async function searchCustomersForRepresentative(search: string, excludeCustomerId: number) { export async function searchCustomersForRepresentative(search: string, excludeCustomerId: number) {
// Sucht Kunden, die als Vertreter hinzugefügt werden können // Sucht Kunden, die als Vertreter hinzugefügt werden können.
// Nur Kunden mit aktiviertem Portal // 2026-07-03: Portal-Filter entfernt – der Admin soll auch Kunden ohne
// Portal-Zugang als Vertreter markieren können; das Portal wird dann
// typischerweise nachträglich aktiviert. Der Zugriff wird ohnehin erst
// aktiv, sobald das Portal für den Vertreter angeschaltet ist – die UI
// zeigt jetzt ein Warn-Badge, wenn der Vertreter noch kein Portal hat.
return prisma.customer.findMany({ return prisma.customer.findMany({
where: { where: {
id: { not: excludeCustomerId }, id: { not: excludeCustomerId },
portalEnabled: true,
OR: [ OR: [
{ firstName: { contains: search } }, { firstName: { contains: search } },
{ lastName: { contains: search } }, { lastName: { contains: search } },
@@ -1005,6 +1012,7 @@ export async function searchCustomersForRepresentative(search: string, excludeCu
lastName: true, lastName: true,
companyName: true, companyName: true,
type: true, type: true,
portalEnabled: true,
}, },
take: 10, take: 10,
}); });
@@ -2327,7 +2327,8 @@ function PortalTab({
</Button> </Button>
</div> </div>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
Nur Kunden mit aktiviertem Portal können als Vertreter hinzugefügt werden. Vertreter ohne Portal-Zugang können bereits hinterlegt werden – der Zugriff
wird erst wirksam, sobald das Portal aktiviert ist.
</p> </p>
{/* Suchergebnisse */} {/* Suchergebnisse */}
@@ -2336,8 +2337,13 @@ function PortalTab({
{searchResults.map((customer) => ( {searchResults.map((customer) => (
<div key={customer.id} className="flex items-center justify-between p-3 hover:bg-gray-50"> <div key={customer.id} className="flex items-center justify-between p-3 hover:bg-gray-50">
<div> <div>
<p className="font-medium"> <p className="font-medium flex items-center gap-2">
{customer.companyName || `${customer.firstName} ${customer.lastName}`} {customer.companyName || `${customer.firstName} ${customer.lastName}`}
{!customer.portalEnabled && (
<Badge variant="warning" className="text-xs">
Portal inaktiv
</Badge>
)}
</p> </p>
<p className="text-sm text-gray-500">{customer.customerNumber}</p> <p className="text-sm text-gray-500">{customer.customerNumber}</p>
</div> </div>
@@ -2362,9 +2368,14 @@ function PortalTab({
{representatives.map((rep: CustomerRepresentative) => ( {representatives.map((rep: CustomerRepresentative) => (
<div key={rep.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg"> <div key={rep.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div> <div>
<p className="font-medium"> <p className="font-medium flex items-center gap-2">
{rep.representative?.companyName || {rep.representative?.companyName ||
`${rep.representative?.firstName} ${rep.representative?.lastName}`} `${rep.representative?.firstName} ${rep.representative?.lastName}`}
{rep.representative && rep.representative.portalEnabled === false && (
<Badge variant="warning" className="text-xs">
Portal inaktiv
</Badge>
)}
</p> </p>
<p className="text-sm text-gray-500">{rep.representative?.customerNumber}</p> <p className="text-sm text-gray-500">{rep.representative?.customerNumber}</p>
</div> </div>
+1
View File
@@ -22,6 +22,7 @@ export interface CustomerSummary {
lastName: string; lastName: string;
companyName?: string; companyName?: string;
type: 'PRIVATE' | 'BUSINESS'; type: 'PRIVATE' | 'BUSINESS';
portalEnabled?: boolean;
} }
export interface RepresentativeAuthorization { export interface RepresentativeAuthorization {