UI: „Einloggen als" je Reseller (Portal-Admin)

ResellersView: pro Reseller ein „Einloggen als" → impersoniert den Reseller-Admin
(login + ROLE_RESELLER_ADMIN der zugehörigen Firma) → Wechsel in Reseller-Kontext.
Eigene Plattform-Org (nur Plattform-Admins) bekommt korrekt keinen Button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Thomas Peterson 2026-06-06 16:02:20 +02:00
parent 44661d9b02
commit 7af4eafcad

View File

@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { list, remove } from '@/api/resources' import { list, remove } from '@/api/resources'
import client from '@/api/client' import client from '@/api/client'
import { useAuthStore } from '@/stores/auth'
import Modal from '@/components/Modal.vue' import Modal from '@/components/Modal.vue'
interface Reseller { interface Reseller {
@ -10,10 +12,34 @@ interface Reseller {
platformPlan: string | null; companies: string[] platformPlan: string | null; companies: string[]
} }
interface Plan { '@id': string; id: string; name: string } 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 }
const auth = useAuthStore()
const router = useRouter()
const resellers = ref<Reseller[]>([]) const resellers = ref<Reseller[]>([])
const plans = ref<Plan[]>([]) const plans = ref<Plan[]>([])
const companies = ref<Company[]>([])
const employees = ref<Employee[]>([])
const busy = ref('')
const loading = ref(true) const loading = ref(true)
// Firma-IRI Reseller-IRI, um den Reseller-Admin 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) =>
e.login && e.roles.includes('ROLE_RESELLER_ADMIN') && companyReseller.value[e.company] === r['@id'],
)
}
async function loginAs(employeeId: string) {
busy.value = employeeId
try {
await auth.impersonate(employeeId)
router.push('/app')
} catch {
alert('Einloggen als … fehlgeschlagen.')
} finally { busy.value = '' }
}
const showForm = ref(false) const showForm = ref(false)
const saving = ref(false) const saving = ref(false)
const error = ref('') const error = ref('')
@ -27,9 +53,11 @@ function slugify(s: string) {
async function load() { async function load() {
loading.value = true loading.value = true
;[resellers.value, plans.value] = await Promise.all([ ;[resellers.value, plans.value, companies.value, employees.value] = await Promise.all([
list<Reseller>('resellers').then((r) => r.member), list<Reseller>('resellers').then((r) => r.member),
list<Plan>('platform_plans').then((r) => r.member).catch(() => []), list<Plan>('platform_plans').then((r) => r.member).catch(() => []),
list<Company>('companies').then((r) => r.member).catch(() => []),
list<Employee>('employees').then((r) => r.member).catch(() => []),
]) ])
loading.value = false loading.value = false
} }
@ -78,7 +106,11 @@ onMounted(load)
<td>{{ r.platformPlan ? (planMap[r.platformPlan] ?? '—') : '—' }}</td> <td>{{ r.platformPlan ? (planMap[r.platformPlan] ?? '—') : '—' }}</td>
<td>{{ r.companies?.length ?? 0 }}</td> <td>{{ r.companies?.length ?? 0 }}</td>
<td><span class="badge" :class="r.status === 'active' ? 'badge-active' : 'badge-inactive'">{{ r.status }}</span></td> <td><span class="badge" :class="r.status === 'active' ? 'badge-active' : 'badge-inactive'">{{ r.status }}</span></td>
<td class="right"><button class="btn btn-ghost btn-sm" @click="del(r)">Löschen</button></td> <td class="right">
<button v-if="resellerAdmin(r)" class="btn btn-soft btn-sm" :disabled="busy !== ''"
@click="loginAs(resellerAdmin(r)!.id)">Einloggen als</button>
<button class="btn btn-ghost btn-sm" @click="del(r)">Löschen</button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>