From cc92b48869474ec3aacb56c5b2d70b628d4be6c7 Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Sat, 6 Jun 2026 18:24:56 +0200 Subject: [PATCH] UI: Reseller aufklappbar (Admins) + Firmen-Aufklapp nur einloggbare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ResellersView: Zeilen aufklappbar → Reseller-Admins (login + ROLE_RESELLER_ADMIN) je „Einloggen als"; Plattform-Org ohne Admins bleibt zu. - CompaniesView: Aufklapp listet nur noch einloggbare Mitarbeiter (login=true), „kein Login"-Zeilen entfallen. Co-Authored-By: Claude Opus 4.8 --- frontend/src/views/CompaniesView.vue | 14 +++--- frontend/src/views/ResellersView.vue | 73 ++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/frontend/src/views/CompaniesView.vue b/frontend/src/views/CompaniesView.vue index 70a1111..e75c82a 100644 --- a/frontend/src/views/CompaniesView.vue +++ b/frontend/src/views/CompaniesView.vue @@ -36,6 +36,10 @@ function slugify(s: string) { function empOf(c: Company) { return employees.value.filter((e) => e.company === c['@id']) } +// Nur Mitarbeiter, deren Login man übernehmen kann +function loginable(c: Company) { + return empOf(c).filter((e) => e.login) +} function activeProfiles(c: Company) { return empOf(c).filter((e) => e.status === 'active').length } @@ -137,18 +141,16 @@ onMounted(load) - + - - + + - diff --git a/frontend/src/views/ResellersView.vue b/frontend/src/views/ResellersView.vue index 9b2544f..017ef70 100644 --- a/frontend/src/views/ResellersView.vue +++ b/frontend/src/views/ResellersView.vue @@ -13,7 +13,7 @@ interface Reseller { } interface Plan { '@id': string; id: string; name: string } interface Company { '@id': string; reseller: string | null } -interface Employee { '@id': string; id: string; company: string; roles: string[]; login: boolean } +interface Employee { '@id': string; id: string; firstName: string; lastName: string; email: string | null; company: string; roles: string[]; login: boolean } const auth = useAuthStore() const router = useRouter() @@ -24,13 +24,17 @@ const employees = ref([]) const busy = ref('') const loading = ref(true) -// Firma-IRI → Reseller-IRI, um den Reseller-Admin je Reseller zu finden +const expanded = ref>({}) +function toggle(r: Reseller) { expanded.value[r.id] = !expanded.value[r.id] } + +// Firma-IRI → Reseller-IRI, um die einloggbaren Reseller-Admins je Reseller zu finden const companyReseller = computed(() => Object.fromEntries(companies.value.map((c) => [c['@id'], c.reseller]))) -function resellerAdmin(r: Reseller): Employee | undefined { - return employees.value.find((e) => +function resellerAdmins(r: Reseller): Employee[] { + return employees.value.filter((e) => e.login && e.roles.includes('ROLE_RESELLER_ADMIN') && companyReseller.value[e.company] === r['@id'], ) } +function resellerAdmin(r: Reseller): Employee | undefined { return resellerAdmins(r)[0] } async function loginAs(employeeId: string) { busy.value = employeeId try { @@ -96,22 +100,40 @@ onMounted(load)
MitarbeiterE-MailTelefonPosition
MitarbeiterE-MailPosition
Keine Mitarbeiter.
Keine einloggbaren Mitarbeiter.
{{ e.firstName }} {{ e.lastName }} {{ e.email || '–' }}{{ e.phone || '–' }} {{ e.position || '–' }} - - kein Login
- + - - - - - - - - - - + + +
NameDomainPaketFirmenStatus
NameDomainPaketFirmenStatus
Lädt…
Noch keine Reseller.
{{ r.name }}
{{ r.slug }}
{{ r.primaryDomain ?? '–' }}{{ r.platformPlan ? (planMap[r.platformPlan] ?? '—') : '—' }}{{ r.companies?.length ?? 0 }}{{ r.status }} - - -
Lädt…
Noch keine Reseller.
@@ -153,8 +175,19 @@ onMounted(load) .tbl td { padding: .9rem 1.2rem; border-bottom: 1px solid #f4f4f4; } .tbl tr:last-child td { border-bottom: none; } .small { font-size: .8rem; } -.right { text-align: right; } +.right { text-align: right; white-space: nowrap; } +.right .btn + .btn { margin-left: .4rem; } .empty { text-align: center; color: var(--muted); padding: 2rem; } +.w-caret { width: 28px; } +.caret { display: inline-block; transition: transform .15s; color: var(--muted); font-size: 1.1rem; } +.caret.open { transform: rotate(90deg); } +.rrow { cursor: pointer; } +.rrow:hover { background: #fafafa; } +.orange { color: var(--psc-orange-dark); } +.subrow > td { background: #fbfbfb; padding: 0 1.2rem 1rem !important; } +.subtbl { width: 100%; border-collapse: collapse; } +.subtbl th { text-align: left; font-size: .68rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); padding: .6rem .7rem; } +.subtbl td { padding: .55rem .7rem; border-top: 1px solid #f0f0f0; } .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; } .divider { font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); font-weight: 700; margin: .4rem 0 .8rem; padding-top: .8rem; border-top: 1px solid var(--line); } .actions { display: flex; justify-content: flex-end; gap: .6rem; margin-top: 1rem; }