From cc8a699e58fb741999155516c1af3c4137477bf6 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 23 Mar 2026 10:29:24 +0100 Subject: [PATCH] fix: billing PREPAY_MANDATE stripe UI, invitation wizard show all feature instances Made-with: Cursor --- src/layouts/MainLayout.module.css | 4 +- src/pages/Dashboard.module.css | 35 ++- src/pages/Store.module.css | 23 +- src/pages/admin/Admin.module.css | 45 ++++ .../admin/AdminUserAccessOverviewPage.tsx | 244 +++++++++++------- .../wizards/AdminInvitationWizardPage.tsx | 17 +- src/pages/billing/BillingDataView.tsx | 20 +- 7 files changed, 276 insertions(+), 112 deletions(-) diff --git a/src/layouts/MainLayout.module.css b/src/layouts/MainLayout.module.css index 15caf06..faf2439 100644 --- a/src/layouts/MainLayout.module.css +++ b/src/layouts/MainLayout.module.css @@ -108,8 +108,10 @@ min-width: 0; display: flex; flex-direction: column; - overflow-x: hidden; + /* Allow horizontal scroll when inner content has a true minimum width; hiding X clips dashboard/store grids on narrow viewports */ + overflow-x: auto; overflow-y: auto; + -webkit-overflow-scrolling: touch; } .mobileTopBar { diff --git a/src/pages/Dashboard.module.css b/src/pages/Dashboard.module.css index f5dc49a..fa96d0c 100644 --- a/src/pages/Dashboard.module.css +++ b/src/pages/Dashboard.module.css @@ -3,9 +3,12 @@ */ .dashboard { - padding: 2rem; + box-sizing: border-box; + width: 100%; max-width: 1200px; margin: 0 auto; + padding: 2rem; + min-width: 0; } /* Header */ @@ -50,10 +53,10 @@ color: var(--text-primary, #1a1a1a); } -/* Instance Grid */ +/* Instance Grid — min(100%, Npx) keeps one fluid column on narrow viewports (no horizontal clip) */ .instanceGrid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); gap: 1rem; } @@ -63,6 +66,7 @@ align-items: center; gap: 1rem; padding: 1.25rem; + min-width: 0; background: var(--surface-color, #ffffff); border: 1px solid var(--border-color, #e0e0e0); border-radius: 12px; @@ -123,9 +127,8 @@ font-size: 1rem; font-weight: 600; color: var(--text-primary, #1a1a1a); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + overflow-wrap: anywhere; + line-height: 1.3; } .mandateName { @@ -245,3 +248,23 @@ :global(.dark-theme) .emptyState p { color: var(--text-secondary-dark, #aaa); } + +@media (max-width: 600px) { + .dashboard { + padding: 1rem 0.875rem; + } + + .header h1 { + font-size: 1.35rem; + } + + .instanceCard { + padding: 1rem; + gap: 0.75rem; + } + + .cardIcon { + width: 40px; + height: 40px; + } +} diff --git a/src/pages/Store.module.css b/src/pages/Store.module.css index c4a67da..6383188 100644 --- a/src/pages/Store.module.css +++ b/src/pages/Store.module.css @@ -3,9 +3,12 @@ */ .store { - padding: 2rem; + box-sizing: border-box; + width: 100%; max-width: 1000px; margin: 0 auto; + padding: 2rem; + min-width: 0; } /* Header */ @@ -39,6 +42,7 @@ border: 1px solid var(--border-color, #e0e0e0); border-radius: 12px; padding: 1.5rem; + min-width: 0; display: flex; flex-direction: column; gap: 1rem; @@ -59,6 +63,7 @@ display: flex; align-items: center; gap: 0.75rem; + min-width: 0; } .cardIcon { @@ -72,6 +77,8 @@ font-size: 1.125rem; font-weight: 600; color: var(--text-primary, #1a1a1a); + min-width: 0; + overflow-wrap: anywhere; } .cardBody { @@ -260,3 +267,17 @@ :global(.dark-theme) .loading { color: var(--text-secondary-dark, #aaa); } + +@media (max-width: 600px) { + .store { + padding: 1rem 0.875rem; + } + + .header h1 { + font-size: 1.35rem; + } + + .card { + padding: 1.125rem; + } +} diff --git a/src/pages/admin/Admin.module.css b/src/pages/admin/Admin.module.css index 20e14f6..a50aa0f 100644 --- a/src/pages/admin/Admin.module.css +++ b/src/pages/admin/Admin.module.css @@ -905,6 +905,51 @@ background: var(--bg-tertiary, #f8f9fa); } +.accessOverviewSubheading { + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-secondary, #666); + margin: 1rem 0 0.5rem; +} + +.accessOverviewSubheading:first-child { + margin-top: 0; +} + +.accessOverviewRoleBullets { + margin: 0.25rem 0 0.85rem; + padding-left: 1.25rem; + font-size: 0.875rem; + line-height: 1.55; +} + +.accessOverviewInstanceStack { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.accessOverviewInstanceBlock { + padding: 0.75rem 1rem; + background: var(--bg-secondary, #f5f5f5); + border-radius: 6px; + border: 1px solid var(--border-color, #e0e0e0); +} + +.accessOverviewInstanceTitle { + font-weight: 600; + font-size: 0.9375rem; + color: var(--text-primary, #1a1a1a); + margin-bottom: 0.35rem; +} + +.accessOverviewInstanceFeature { + font-weight: 500; + color: var(--text-secondary, #666); +} + .emptyHint { font-size: 0.875rem; color: var(--text-tertiary, #999); diff --git a/src/pages/admin/AdminUserAccessOverviewPage.tsx b/src/pages/admin/AdminUserAccessOverviewPage.tsx index 72adbc6..fdea742 100644 --- a/src/pages/admin/AdminUserAccessOverviewPage.tsx +++ b/src/pages/admin/AdminUserAccessOverviewPage.tsx @@ -47,7 +47,7 @@ interface AccessEntry { interface MandateInfo { id: string; name: string; - label?: string; + label?: string | null; roleIds: string[]; featureInstances: { id: string; @@ -58,6 +58,18 @@ interface MandateInfo { }[]; } +function _mandateNameLine(mandate: MandateInfo): string { + const label = mandate.label?.trim(); + if (label) { + return `${mandate.name} (${label})`; + } + return mandate.name; +} + +function _roleDescriptionLine(role: RoleInfo): string { + return role.description?.de || role.description?.en || ''; +} + interface UserAccessOverview { user: UserOption; isSysAdmin: boolean; @@ -174,9 +186,14 @@ export const AdminUserAccessOverviewPage: React.FC = () => { const renderOverviewTab = () => { if (!overview) return null; + const roleById = new Map(overview.roles.map((r) => [r.id, r])); + const globalRoles = overview.roles.filter((r) => r.scope === 'global'); + + const _resolveRoles = (roleIds: string[]): RoleInfo[] => + roleIds.map((id) => roleById.get(id)).filter((r): r is RoleInfo => !!r); + return (
- {/* SysAdmin Notice */} {overview.isSysAdmin && (
@@ -184,109 +201,148 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
)} - {/* Mandates & Feature Instances */} -

Mandate & Feature-Instanzen

+

Zugriff nach Mandant

+ {overview.mandates.length === 0 ? (

Keine Mandate-Zuordnungen vorhanden.

) : (
- {overview.mandates.map(mandate => ( -
-
toggleMandate(mandate.id)} - > -
- {expandedMandates.has(mandate.id) ? : } - {mandate.label || mandate.name} - - {mandate.featureInstances.length} Feature-Instanz(en) - + {overview.mandates.map((mandate) => { + const mandateRoles = _resolveRoles(mandate.roleIds); + + return ( +
+
toggleMandate(mandate.id)}> +
+ {expandedMandates.has(mandate.id) ? ( + + ) : ( + + )} + {_mandateNameLine(mandate)} + + {mandateRoles.length} Mandantenrolle(n) · {mandate.featureInstances.length} Feature-Instanz(en) + +
+ {expandedMandates.has(mandate.id) && ( +
+ {mandateRoles.length === 0 ? ( +

Keine Rollen direkt am Mandanten.

+ ) : ( +
    + {mandateRoles.map((r) => ( +
  • + {r.roleLabel} + + {r.scope} + +
  • + ))} +
+ )} + +
Feature-Instanzen
+ {mandate.featureInstances.length === 0 ? ( +

Keine Feature-Instanzen zugewiesen.

+ ) : ( +
+ {mandate.featureInstances.map((instance) => { + const instanceRoles = _resolveRoles(instance.roleIds); + const featureTitle = instance.featureLabel?.de || instance.featureCode; + return ( +
+
+ {instance.label}{' '} + ({featureTitle}) +
+ {instanceRoles.length === 0 ? ( +

+ Keine Rollen. +

+ ) : ( +
    + {instanceRoles.map((r) => ( +
  • + {r.roleLabel} + + {r.scope} + +
  • + ))} +
+ )} +
+ ); + })} +
+ )} +
+ )}
- {expandedMandates.has(mandate.id) && ( -
- {mandate.featureInstances.length === 0 ? ( -

Keine Feature-Instanzen zugewiesen.

- ) : ( -
- {mandate.featureInstances.map(instance => ( -
-
- {instance.label} -
-
- Feature: {instance.featureLabel?.de || instance.featureCode} -
-
- Rollen: {instance.roleIds.length > 0 - ? overview.roles - .filter(r => instance.roleIds.includes(r.id)) - .map(r => r.roleLabel) - .join(', ') - : 'Keine' - } -
-
- ))} -
- )} -
- )} -
- ))} + ); + })}
)} - {/* Roles */} -

Zugewiesene Rollen

- {overview.roles.length === 0 ? ( -

Keine Rollen zugewiesen.

- ) : ( -
- {overview.roles.map(role => ( -
-
toggleRole(role.id)} - > -
- {expandedRoles.has(role.id) ? : } - {role.roleLabel} - - {role.scope} - -
-
- {expandedRoles.has(role.id) && ( -
-
-

Beschreibung: {role.description?.de || role.description?.en || '-'}

-

Quelle: {role.source === 'mandate' - ? `Mandate: ${role.sourceMandateName}` - : `Feature-Instanz: ${role.sourceInstanceLabel}` - }

+ {globalRoles.length > 0 && ( + <> +

+ Globale Rollen +

+

+ Nicht an einen Mandanten gebunden. +

+
+ {globalRoles.map((role) => ( +
+
toggleRole(role.id)}> +
+ {expandedRoles.has(role.id) ? ( + + ) : ( + + )} + {role.roleLabel} + + {role.scope} +
- )} -
- ))} -
+ {expandedRoles.has(role.id) && ( +
+
+

+ Beschreibung: {_roleDescriptionLine(role) || '—'} +

+
+
+ )} +
+ ))} +
+ )}
); diff --git a/src/pages/admin/wizards/AdminInvitationWizardPage.tsx b/src/pages/admin/wizards/AdminInvitationWizardPage.tsx index 79fe0a5..fffc5b5 100644 --- a/src/pages/admin/wizards/AdminInvitationWizardPage.tsx +++ b/src/pages/admin/wizards/AdminInvitationWizardPage.tsx @@ -128,9 +128,8 @@ export const AdminInvitationWizardPage: React.FC = () => { useEffect(() => { if (inviteType === 'featureInstance' && selectedMandate) { - fetchInstances(selectedMandate.id).then(data => - setInstances((data || []).filter((i: FeatureInstance) => i.enabled)) - ); + // Show all instances for the mandate (including disabled). Previously only `enabled` instances were listed, which hid deactivated instances from admins. + fetchInstances(selectedMandate.id).then(data => setInstances(data || [])); } else { setInstances([]); setSelectedInstance(null); @@ -411,7 +410,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
{instances.length === 0 ? ( -

Keine aktiven Feature-Instanzen für diesen Mandanten vorhanden.

+

Keine Feature-Instanzen für diesen Mandanten vorhanden.

) : ( )}
diff --git a/src/pages/billing/BillingDataView.tsx b/src/pages/billing/BillingDataView.tsx index 1d2571f..feb44f1 100644 --- a/src/pages/billing/BillingDataView.tsx +++ b/src/pages/billing/BillingDataView.tsx @@ -65,8 +65,9 @@ const BalanceCard: React.FC = ({ balance, onCheckout, checkout return 'Prepaid (Mandant)'; }; - const canTopUp = balance.billingModel === 'PREPAY_USER' - || balance.billingModel === 'PREPAY_MANDATE'; + // Stripe top-up on this page: only personal prepaid wallets. Mandate pool (PREPAY_MANDATE) is topped up by mandate admins via Administration → Billing. + const canStripeTopUpHere = balance.billingModel === 'PREPAY_USER'; + const isMandatePrepaidPool = balance.billingModel === 'PREPAY_MANDATE'; return (
@@ -82,7 +83,20 @@ const BalanceCard: React.FC = ({ balance, onCheckout, checkout Niedriges Guthaben
)} - {canTopUp && onCheckout && ( + {isMandatePrepaidPool && ( +

+ Aufladung des Mandanten-Guthabens ist nur für Mandanten-Administratoren möglich (Menü Administration → Billing). +

+ )} + {canStripeTopUpHere && onCheckout && (
{!showCheckout ? (