1247 lines
52 KiB
Vue
1247 lines
52 KiB
Vue
<template>
|
|
<v-app>
|
|
<!-- App Bar (same as dashboard for consistency) -->
|
|
<v-app-bar color="primary" prominent>
|
|
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
|
|
|
|
<v-toolbar-title class="text-h5 font-weight-bold">
|
|
<v-icon icon="mdi-account-group" class="mr-2"></v-icon>
|
|
User Management
|
|
<v-chip class="ml-2" :color="roleColor" size="small">
|
|
{{ userRole }} • {{ scopeLevel }}
|
|
</v-chip>
|
|
</v-toolbar-title>
|
|
|
|
<v-spacer></v-spacer>
|
|
|
|
<!-- Navigation buttons -->
|
|
<v-btn icon @click="navigateTo('/dashboard')" title="Back to Dashboard">
|
|
<v-icon icon="mdi-view-dashboard"></v-icon>
|
|
</v-btn>
|
|
|
|
<!-- User Menu -->
|
|
<v-menu>
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn icon v-bind="props">
|
|
<v-avatar size="40" color="secondary">
|
|
<v-icon icon="mdi-account"></v-icon>
|
|
</v-avatar>
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item>
|
|
<v-list-item-title class="font-weight-bold">
|
|
{{ userEmail }}
|
|
</v-list-item-title>
|
|
<v-list-item-subtitle>
|
|
Rank: {{ userRank }} • Scope ID: {{ scopeId }}
|
|
</v-list-item-subtitle>
|
|
</v-list-item>
|
|
<v-divider></v-divider>
|
|
<v-list-item @click="navigateTo('/profile')">
|
|
<v-list-item-title>
|
|
<v-icon icon="mdi-account-cog" class="mr-2"></v-icon>
|
|
Profile Settings
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
<v-list-item @click="logout">
|
|
<v-list-item-title class="text-error">
|
|
<v-icon icon="mdi-logout" class="mr-2"></v-icon>
|
|
Logout
|
|
</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-menu>
|
|
</v-app-bar>
|
|
|
|
<!-- Navigation Drawer -->
|
|
<v-navigation-drawer v-model="drawer" temporary>
|
|
<v-list>
|
|
<v-list-item @click="navigateTo('/dashboard')" prepend-icon="mdi-view-dashboard">
|
|
<v-list-item-title>Dashboard</v-list-item-title>
|
|
</v-list-item>
|
|
|
|
<v-list-group value="Admin">
|
|
<template v-slot:activator="{ props }">
|
|
<v-list-item v-bind="props" prepend-icon="mdi-shield-account">
|
|
<v-list-item-title>Administration</v-list-item-title>
|
|
</v-list-item>
|
|
</template>
|
|
|
|
<v-list-item @click="navigateTo('/users')" prepend-icon="mdi-account-group" active>
|
|
<v-list-item-title>User Management</v-list-item-title>
|
|
</v-list-item>
|
|
|
|
<v-list-item @click="navigateTo('/organizations')" prepend-icon="mdi-office-building">
|
|
<v-list-item-title>Organizations</v-list-item-title>
|
|
</v-list-item>
|
|
|
|
<v-list-item @click="navigateTo('/system-health')" prepend-icon="mdi-heart-pulse">
|
|
<v-list-item-title>System Health</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list-group>
|
|
|
|
<v-list-group value="Monitoring">
|
|
<template v-slot:activator="{ props }">
|
|
<v-list-item v-bind="props" prepend-icon="mdi-monitor-dashboard">
|
|
<v-list-item-title>Monitoring</v-list-item-title>
|
|
</v-list-item>
|
|
</template>
|
|
|
|
<v-list-item @click="navigateTo('/ai-logs')" prepend-icon="mdi-robot">
|
|
<v-list-item-title>AI Logs</v-list-item-title>
|
|
</v-list-item>
|
|
|
|
<v-list-item @click="navigateTo('/audit-trail')" prepend-icon="mdi-history">
|
|
<v-list-item-title>Audit Trail</v-list-item-title>
|
|
</v-list-item>
|
|
</v-list-group>
|
|
</v-list>
|
|
</v-navigation-drawer>
|
|
|
|
<!-- Main Content -->
|
|
<v-main>
|
|
<v-container fluid class="pa-6">
|
|
<!-- Page Header -->
|
|
<v-row class="mb-6">
|
|
<v-col cols="12">
|
|
<v-card class="pa-4" color="primary" dark>
|
|
<v-card-title class="text-h4">
|
|
<v-icon icon="mdi-account-group" class="mr-3"></v-icon>
|
|
User Management
|
|
</v-card-title>
|
|
<v-card-subtitle class="text-h6">
|
|
Manage user roles, permissions, and geographical scopes
|
|
</v-card-subtitle>
|
|
<v-card-text>
|
|
<div class="d-flex align-center">
|
|
<v-chip class="mr-2" color="white" text-color="primary">
|
|
<v-icon icon="mdi-shield-account" class="mr-1"></v-icon>
|
|
Access: Superadmin & Admin only
|
|
</v-chip>
|
|
<v-chip class="mr-2" color="white" text-color="primary">
|
|
<v-icon icon="mdi-earth" class="mr-1"></v-icon>
|
|
Scope Level: {{ scopeLevel }}
|
|
</v-chip>
|
|
<v-chip color="white" text-color="primary">
|
|
<v-icon icon="mdi-account" class="mr-1"></v-icon>
|
|
Total Users: {{ users.length }}
|
|
</v-chip>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Geographical Scope Filter -->
|
|
<v-row class="mb-4">
|
|
<v-col cols="12">
|
|
<v-card class="pa-4">
|
|
<v-card-title class="text-h6">
|
|
<v-icon icon="mdi-earth" class="mr-2"></v-icon>
|
|
Geographical Scope Filter
|
|
</v-card-title>
|
|
<v-card-subtitle>
|
|
Filter users by their geographical scope and region
|
|
</v-card-subtitle>
|
|
<v-card-text>
|
|
<div class="d-flex align-center flex-wrap">
|
|
<v-select
|
|
v-model="selectedGeographicalScope"
|
|
:items="geographicalScopes"
|
|
item-title="label"
|
|
item-value="value"
|
|
label="Select Scope"
|
|
prepend-icon="mdi-filter"
|
|
style="max-width: 300px;"
|
|
class="mr-4"
|
|
clearable
|
|
>
|
|
<template v-slot:item="{ props, item }">
|
|
<v-list-item v-bind="props">
|
|
<template v-slot:prepend>
|
|
<v-icon :icon="item.raw.icon"></v-icon>
|
|
</template>
|
|
<v-list-item-title>{{ item.raw.label }}</v-list-item-title>
|
|
<v-list-item-subtitle>{{ item.raw.description }}</v-list-item-subtitle>
|
|
</v-list-item>
|
|
</template>
|
|
</v-select>
|
|
|
|
<v-chip-group v-model="selectedGeographicalScope" mandatory class="ml-4">
|
|
<v-chip
|
|
v-for="scope in geographicalScopes"
|
|
:key="scope.value"
|
|
:value="scope.value"
|
|
:prepend-icon="scope.icon"
|
|
:color="selectedGeographicalScope === scope.value ? 'primary' : 'default'"
|
|
variant="outlined"
|
|
>
|
|
{{ scope.label }}
|
|
</v-chip>
|
|
</v-chip-group>
|
|
|
|
<v-spacer></v-spacer>
|
|
|
|
<div class="text-body-2 text-grey">
|
|
<v-icon icon="mdi-information" class="mr-1" size="small"></v-icon>
|
|
Showing users in: <strong>{{ getScopeDisplayName(selectedGeographicalScope) }}</strong>
|
|
({{ filteredUsers.length }} of {{ users.length }} total users)
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scope Hierarchy Display -->
|
|
<v-alert v-if="selectedGeographicalScope && selectedGeographicalScope !== 'Global'" type="info" variant="tonal" class="mt-4">
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-sitemap" class="mr-2"></v-icon>
|
|
<div>
|
|
<strong>Scope Hierarchy:</strong>
|
|
<span class="ml-2">
|
|
Global → Hungary
|
|
<span v-if="selectedGeographicalScope === 'Pest County' || selectedGeographicalScope === 'Budapest' || selectedGeographicalScope === 'District V'"> → Pest County</span>
|
|
<span v-if="selectedGeographicalScope === 'Budapest' || selectedGeographicalScope === 'District V'"> → Budapest</span>
|
|
<span v-if="selectedGeographicalScope === 'District V'"> → District V</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</v-alert>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- User Data Table -->
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<v-card>
|
|
<v-card-title class="d-flex justify-space-between align-center">
|
|
<div>
|
|
<v-icon icon="mdi-account-details" class="mr-2"></v-icon>
|
|
User List
|
|
<v-chip class="ml-2" color="primary" size="small">
|
|
{{ filteredUsers.length }} users
|
|
</v-chip>
|
|
</div>
|
|
<div>
|
|
<v-text-field
|
|
v-model="search"
|
|
prepend-inner-icon="mdi-magnify"
|
|
label="Search users..."
|
|
single-line
|
|
hide-details
|
|
class="mr-4"
|
|
style="max-width: 300px;"
|
|
></v-text-field>
|
|
</div>
|
|
</v-card-title>
|
|
|
|
<v-card-text>
|
|
<!-- User Data Table -->
|
|
<v-data-table
|
|
:headers="headers"
|
|
:items="filteredUsers"
|
|
:search="search"
|
|
:items-per-page="10"
|
|
class="elevation-1"
|
|
hover
|
|
>
|
|
<!-- Email Column -->
|
|
<template v-slot:item.email="{ item }">
|
|
<div class="d-flex align-center">
|
|
<v-avatar size="32" color="blue-lighten-5" class="mr-3">
|
|
<v-icon icon="mdi-email"></v-icon>
|
|
</v-avatar>
|
|
<div>
|
|
<div class="font-weight-medium">{{ item.email }}</div>
|
|
<div class="text-caption text-grey">ID: {{ item.id }}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Role Column -->
|
|
<template v-slot:item.role="{ item }">
|
|
<v-chip :color="getRoleColor(item.role)" size="small" dark>
|
|
<v-icon start :icon="getRoleIcon(item.role)" size="16"></v-icon>
|
|
{{ formatRole(item.role) }}
|
|
</v-chip>
|
|
</template>
|
|
|
|
<!-- Scope Level Column -->
|
|
<template v-slot:item.scope_level="{ item }">
|
|
<v-chip :color="getScopeColor(item.scope_level)" size="small" variant="outlined">
|
|
<v-icon start :icon="getScopeIcon(item.scope_level)" size="16"></v-icon>
|
|
{{ item.scope_level }}
|
|
</v-chip>
|
|
</template>
|
|
|
|
<!-- Region/Country Column -->
|
|
<template v-slot:item.region_display="{ item }">
|
|
<div class="d-flex align-center">
|
|
<v-icon v-if="item.scope_level === 'Global'" icon="mdi-earth" color="purple" class="mr-2"></v-icon>
|
|
<v-icon v-else-if="item.country_code === 'HU'" icon="mdi-flag" color="blue" class="mr-2"></v-icon>
|
|
<v-icon v-else icon="mdi-map-marker" color="green" class="mr-2"></v-icon>
|
|
<div>
|
|
<div class="font-weight-medium">{{ getRegionDisplay(item) }}</div>
|
|
<div v-if="item.region_code && item.country_code" class="text-caption text-grey">
|
|
{{ item.region_code }} • {{ item.country_code }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Status Column -->
|
|
<template v-slot:item.status="{ item }">
|
|
<v-chip :color="item.status === 'active' ? 'green' : 'grey'" size="small" :variant="item.status === 'active' ? 'flat' : 'outlined'">
|
|
<v-icon start :icon="item.status === 'active' ? 'mdi-check-circle' : 'mdi-account-off'" size="16"></v-icon>
|
|
{{ item.status === 'active' ? 'Active' : 'Inactive' }}
|
|
</v-chip>
|
|
</template>
|
|
|
|
<!-- Actions Column -->
|
|
<template v-slot:item.actions="{ item }">
|
|
<div class="d-flex">
|
|
<v-tooltip text="Edit Role" location="top">
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn
|
|
icon
|
|
size="small"
|
|
color="primary"
|
|
v-bind="props"
|
|
@click="openEditDialog(item)"
|
|
:disabled="!canEditUser(item)"
|
|
>
|
|
<v-icon icon="mdi-account-cog"></v-icon>
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip :text="item.status === 'active' ? 'Deactivate User' : 'Activate User'" location="top">
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn
|
|
icon
|
|
size="small"
|
|
:color="item.status === 'active' ? 'orange' : 'green'"
|
|
v-bind="props"
|
|
@click="toggleUserStatus(item)"
|
|
class="ml-1"
|
|
>
|
|
<v-icon :icon="item.status === 'active' ? 'mdi-account-off' : 'mdi-account-check'"></v-icon>
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
|
|
<v-tooltip text="View Details" location="top">
|
|
<template v-slot:activator="{ props }">
|
|
<v-btn
|
|
icon
|
|
size="small"
|
|
color="info"
|
|
v-bind="props"
|
|
@click="viewUserDetails(item)"
|
|
class="ml-1"
|
|
>
|
|
<v-icon icon="mdi-eye"></v-icon>
|
|
</v-btn>
|
|
</template>
|
|
</v-tooltip>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- No data state -->
|
|
<template v-slot:no-data>
|
|
<div class="text-center py-6">
|
|
<v-icon icon="mdi-account-group-off" size="64" class="mb-4" color="grey-lighten-1"></v-icon>
|
|
<h3 class="text-h5 mb-2">No users found</h3>
|
|
<p class="text-body-1 text-grey mb-4">
|
|
Try adjusting your search or load mock data for testing
|
|
</p>
|
|
<v-btn color="primary" @click="loadMockData">
|
|
<v-icon icon="mdi-database" class="mr-2"></v-icon>
|
|
Load Mock Data
|
|
</v-btn>
|
|
</div>
|
|
</template>
|
|
</v-data-table>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Moderation Queue -->
|
|
<v-row class="mt-6">
|
|
<v-col cols="12">
|
|
<v-card>
|
|
<v-card-title class="bg-amber-lighten-5">
|
|
<v-icon icon="mdi-clock-alert" class="mr-2"></v-icon>
|
|
Moderation Queue - Pending Approvals
|
|
<v-chip class="ml-2" color="amber" size="small">
|
|
{{ pendingApprovals.length }} pending
|
|
</v-chip>
|
|
</v-card-title>
|
|
<v-card-subtitle>
|
|
Users and services waiting for authorization within the selected geographical scope
|
|
</v-card-subtitle>
|
|
<v-card-text>
|
|
<!-- Tabs for different approval types -->
|
|
<v-tabs v-model="moderationTab" class="mb-4">
|
|
<v-tab value="users">Pending Users</v-tab>
|
|
<v-tab value="services">Pending Services</v-tab>
|
|
<v-tab value="organizations">Pending Organizations</v-tab>
|
|
</v-tabs>
|
|
|
|
<v-window v-model="moderationTab">
|
|
<!-- Pending Users Tab -->
|
|
<v-window-item value="users">
|
|
<v-data-table
|
|
:headers="pendingUsersHeaders"
|
|
:items="pendingUsers"
|
|
:items-per-page="5"
|
|
class="elevation-1"
|
|
>
|
|
<template v-slot:item.user="{ item }">
|
|
<div class="d-flex align-center">
|
|
<v-avatar size="32" color="blue-lighten-5" class="mr-3">
|
|
<v-icon icon="mdi-account"></v-icon>
|
|
</v-avatar>
|
|
<div>
|
|
<div class="font-weight-medium">{{ item.email }}</div>
|
|
<div class="text-caption text-grey">Requested role: {{ item.requested_role }}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-slot:item.scope="{ item }">
|
|
<v-chip :color="getScopeColor(item.scope_level)" size="small" variant="outlined">
|
|
{{ item.scope_level }}
|
|
</v-chip>
|
|
<div class="text-caption text-grey mt-1">{{ item.region_code || item.country_code || 'Global' }}</div>
|
|
</template>
|
|
|
|
<template v-slot:item.requested_at="{ item }">
|
|
{{ item.requested_at }}
|
|
</template>
|
|
|
|
<template v-slot:item.actions="{ item }">
|
|
<div class="d-flex">
|
|
<v-btn
|
|
size="small"
|
|
color="green"
|
|
variant="tonal"
|
|
@click="approvePendingItem(item, 'user')"
|
|
class="mr-2"
|
|
>
|
|
<v-icon icon="mdi-check" size="16" class="mr-1"></v-icon>
|
|
Approve
|
|
</v-btn>
|
|
<v-btn
|
|
size="small"
|
|
color="red"
|
|
variant="tonal"
|
|
@click="rejectPendingItem(item, 'user')"
|
|
>
|
|
<v-icon icon="mdi-close" size="16" class="mr-1"></v-icon>
|
|
Reject
|
|
</v-btn>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-slot:no-data>
|
|
<div class="text-center py-6">
|
|
<v-icon icon="mdi-check-circle" size="64" class="mb-4" color="green-lighten-1"></v-icon>
|
|
<h3 class="text-h5 mb-2">No pending user approvals</h3>
|
|
<p class="text-body-1 text-grey">
|
|
All user registration requests have been processed.
|
|
</p>
|
|
</div>
|
|
</template>
|
|
</v-data-table>
|
|
</v-window-item>
|
|
|
|
<!-- Pending Services Tab -->
|
|
<v-window-item value="services">
|
|
<v-alert type="info" variant="tonal" class="mb-4">
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-information" class="mr-2"></v-icon>
|
|
<div>
|
|
Service approval functionality will be implemented in Phase 5.
|
|
<div class="text-caption">Mock data shown for demonstration.</div>
|
|
</div>
|
|
</div>
|
|
</v-alert>
|
|
|
|
<v-data-table
|
|
:headers="pendingServicesHeaders"
|
|
:items="pendingServices"
|
|
:items-per-page="5"
|
|
class="elevation-1"
|
|
>
|
|
<template v-slot:item.service="{ item }">
|
|
<div class="d-flex align-center">
|
|
<v-avatar size="32" color="green-lighten-5" class="mr-3">
|
|
<v-icon icon="mdi-wrench"></v-icon>
|
|
</v-avatar>
|
|
<div>
|
|
<div class="font-weight-medium">{{ item.name }}</div>
|
|
<div class="text-caption text-grey">Category: {{ item.category }}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-slot:item.actions="{ item }">
|
|
<div class="d-flex">
|
|
<v-btn
|
|
size="small"
|
|
color="green"
|
|
variant="tonal"
|
|
@click="approvePendingItem(item, 'service')"
|
|
class="mr-2"
|
|
>
|
|
Approve
|
|
</v-btn>
|
|
<v-btn
|
|
size="small"
|
|
color="red"
|
|
variant="tonal"
|
|
@click="rejectPendingItem(item, 'service')"
|
|
>
|
|
Reject
|
|
</v-btn>
|
|
</div>
|
|
</template>
|
|
</v-data-table>
|
|
</v-window-item>
|
|
|
|
<!-- Pending Organizations Tab -->
|
|
<v-window-item value="organizations">
|
|
<v-alert type="info" variant="tonal">
|
|
Organization approval functionality will be implemented in Phase 6.
|
|
</v-alert>
|
|
</v-window-item>
|
|
</v-window>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- RBAC Information Panel -->
|
|
<v-row class="mt-4">
|
|
<v-col cols="12" md="6">
|
|
<v-card>
|
|
<v-card-title class="bg-blue-lighten-5">
|
|
<v-icon icon="mdi-shield-account" class="mr-2"></v-icon>
|
|
Role Hierarchy
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-list>
|
|
<v-list-item v-for="role in roleHierarchy" :key="role.name">
|
|
<template v-slot:prepend>
|
|
<v-icon :color="role.color">{{ role.icon }}</v-icon>
|
|
</template>
|
|
<v-list-item-title>{{ role.name }}</v-list-item-title>
|
|
<v-list-item-subtitle>Rank: {{ role.rank }} • {{ role.description }}</v-list-item-subtitle>
|
|
<template v-slot:append>
|
|
<v-chip :color="role.color" size="small" dark>
|
|
{{ role.scope }}
|
|
</v-chip>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<v-card>
|
|
<v-card-title class="bg-green-lighten-5">
|
|
<v-icon icon="mdi-earth" class="mr-2"></v-icon>
|
|
Scope Levels
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<v-list>
|
|
<v-list-item v-for="scope in scopeLevels" :key="scope.level">
|
|
<template v-slot:prepend>
|
|
<v-icon :color="scope.color">{{ scope.icon }}</v-icon>
|
|
</template>
|
|
<v-list-item-title>{{ scope.level }}</v-list-item-title>
|
|
<v-list-item-subtitle>{{ scope.description }}</v-list-item-subtitle>
|
|
<template v-slot:append>
|
|
<v-chip :color="scope.color" size="small" dark>
|
|
{{ scope.example }}
|
|
</v-chip>
|
|
</template>
|
|
</v-list-item>
|
|
</v-list>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
</v-main>
|
|
|
|
<!-- Edit Role Dialog -->
|
|
<v-dialog v-model="editDialog" max-width="600px">
|
|
<v-card v-if="selectedUser">
|
|
<v-card-title class="bg-primary" dark>
|
|
<v-icon icon="mdi-account-cog" class="mr-2"></v-icon>
|
|
Edit User Role & Scope
|
|
</v-card-title>
|
|
|
|
<v-card-text class="pa-6">
|
|
<!-- User Info -->
|
|
<v-row class="mb-4">
|
|
<v-col cols="12">
|
|
<div class="d-flex align-center">
|
|
<v-avatar size="64" color="blue-lighten-5" class="mr-4">
|
|
<v-icon icon="mdi-account" size="32"></v-icon>
|
|
</v-avatar>
|
|
<div>
|
|
<h3 class="text-h5">{{ selectedUser.email }}</h3>
|
|
<div class="text-body-1 text-grey">
|
|
User ID: {{ selectedUser.id }} • Created: {{ selectedUser.created_at }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Current Status -->
|
|
<v-row class="mb-2">
|
|
<v-col cols="12">
|
|
<v-alert type="info" variant="tonal" class="mb-4">
|
|
<div class="d-flex justify-space-between align-center">
|
|
<div>
|
|
<strong>Current:</strong>
|
|
<v-chip class="ml-2" :color="getRoleColor(selectedUser.role)" size="small" dark>
|
|
{{ formatRole(selectedUser.role) }}
|
|
</v-chip>
|
|
<v-chip class="ml-2" :color="getScopeColor(selectedUser.scope_level)" size="small" variant="outlined">
|
|
{{ selectedUser.scope_level }}
|
|
</v-chip>
|
|
</div>
|
|
<v-chip :color="selectedUser.status === 'active' ? 'green' : 'grey'" size="small">
|
|
{{ selectedUser.status === 'active' ? 'Active' : 'Inactive' }}
|
|
</v-chip>
|
|
</div>
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Role Selection -->
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-card variant="outlined" class="pa-4">
|
|
<v-card-title class="text-h6">Select Role</v-card-title>
|
|
<v-card-text>
|
|
<v-radio-group v-model="selectedUser.role">
|
|
<v-radio value="superadmin" color="red">
|
|
<template v-slot:label>
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-crown" color="red" class="mr-2"></v-icon>
|
|
<div>
|
|
<div class="font-weight-bold">Superadmin</div>
|
|
<div class="text-caption text-grey">Full system access (Rank: 10)</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</v-radio>
|
|
|
|
<v-radio value="admin" color="orange">
|
|
<template v-slot:label>
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-shield-account" color="orange" class="mr-2"></v-icon>
|
|
<div>
|
|
<div class="font-weight-bold">Admin</div>
|
|
<div class="text-caption text-grey">Administrative access (Rank: 7)</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</v-radio>
|
|
|
|
<v-radio value="moderator" color="blue">
|
|
<template v-slot:label>
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-account-tie" color="blue" class="mr-2"></v-icon>
|
|
<div>
|
|
<div class="font-weight-bold">Moderator</div>
|
|
<div class="text-caption text-grey">Content moderation (Rank: 5)</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</v-radio>
|
|
|
|
<v-radio value="sales_agent" color="green">
|
|
<template v-slot:label>
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-account-cash" color="green" class="mr-2"></v-icon>
|
|
<div>
|
|
<div class="font-weight-bold">Sales Agent</div>
|
|
<div class="text-caption text-grey">Sales & customer service (Rank: 3)</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</v-radio>
|
|
</v-radio-group>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<v-card variant="outlined" class="pa-4">
|
|
<v-card-title class="text-h6">Select Scope Level</v-card-title>
|
|
<v-card-text>
|
|
<v-select
|
|
v-model="selectedUser.scope_level"
|
|
:items="scopeLevels"
|
|
item-title="level"
|
|
item-value="level"
|
|
label="Scope Level"
|
|
prepend-icon="mdi-earth"
|
|
clearable
|
|
>
|
|
<template v-slot:item="{ props, item }">
|
|
<v-list-item v-bind="props">
|
|
<template v-slot:prepend>
|
|
<v-icon :color="item.raw.color">{{ item.raw.icon }}</v-icon>
|
|
</template>
|
|
<v-list-item-title>{{ item.raw.level }}</v-list-item-title>
|
|
<v-list-item-subtitle>{{ item.raw.description }}</v-list-item-subtitle>
|
|
</v-list-item>
|
|
</template>
|
|
</v-select>
|
|
|
|
<!-- Geographical Scope Picker (dynamic based on role and scope level) -->
|
|
<div v-if="showGeographicalScopePicker" class="mt-4">
|
|
<v-alert type="info" variant="tonal" class="mb-4">
|
|
<div class="d-flex align-center">
|
|
<v-icon icon="mdi-map-marker-radius" class="mr-2"></v-icon>
|
|
<div>
|
|
<strong>Geographical Scope Required</strong>
|
|
<div class="text-caption">
|
|
This role ({{ selectedUser.role }}) requires a geographical scope to define the user's territory.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</v-alert>
|
|
|
|
<!-- Country Code Field -->
|
|
<div v-if="selectedUser.scope_level === 'Country' || selectedUser.scope_level === 'Region' || selectedUser.scope_level === 'City' || selectedUser.scope_level === 'District'" class="mb-4">
|
|
<v-select
|
|
v-model="selectedUser.country_code"
|
|
:items="availableCountries"
|
|
item-title="name"
|
|
item-value="code"
|
|
label="Country"
|
|
prepend-icon="mdi-flag"
|
|
clearable
|
|
:required="selectedUser.scope_level !== 'Global'"
|
|
>
|
|
<template v-slot:item="{ props, item }">
|
|
<v-list-item v-bind="props">
|
|
<template v-slot:prepend>
|
|
<v-icon :icon="getCountryIcon(item.raw.code)"></v-icon>
|
|
</template>
|
|
<v-list-item-title>{{ item.raw.name }}</v-list-item-title>
|
|
<v-list-item-subtitle>{{ item.raw.code }}</v-list-item-subtitle>
|
|
</v-list-item>
|
|
</template>
|
|
</v-select>
|
|
</div>
|
|
|
|
<!-- Region Code Field (for Region, City, District) -->
|
|
<div v-if="selectedUser.scope_level === 'Region' || selectedUser.scope_level === 'City' || selectedUser.scope_level === 'District'" class="mb-4">
|
|
<v-select
|
|
v-model="selectedUser.region_code"
|
|
:items="availableRegions"
|
|
item-title="name"
|
|
item-value="code"
|
|
label="Region"
|
|
prepend-icon="mdi-map-marker-radius"
|
|
clearable
|
|
:disabled="!selectedUser.country_code"
|
|
>
|
|
<template v-slot:item="{ props, item }">
|
|
<v-list-item v-bind="props">
|
|
<v-list-item-title>{{ item.raw.name }}</v-list-item-title>
|
|
<v-list-item-subtitle>{{ item.raw.code }}</v-list-item-subtitle>
|
|
</v-list-item>
|
|
</template>
|
|
</v-select>
|
|
</div>
|
|
|
|
<!-- City/District Field (for City, District) -->
|
|
<div v-if="selectedUser.scope_level === 'City' || selectedUser.scope_level === 'District'" class="mb-4">
|
|
<v-text-field
|
|
v-model="selectedUser.city_name"
|
|
label="City Name"
|
|
placeholder="e.g., Budapest, Berlin"
|
|
prepend-icon="mdi-city"
|
|
:disabled="!selectedUser.region_code"
|
|
></v-text-field>
|
|
</div>
|
|
|
|
<!-- District Field (for District only) -->
|
|
<div v-if="selectedUser.scope_level === 'District'" class="mb-4">
|
|
<v-text-field
|
|
v-model="selectedUser.district_name"
|
|
label="District Name"
|
|
placeholder="e.g., District V, Mitte"
|
|
prepend-icon="mdi-map-marker"
|
|
:disabled="!selectedUser.city_name"
|
|
></v-text-field>
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Status Toggle -->
|
|
<v-row class="mt-2">
|
|
<v-col cols="12">
|
|
<v-switch
|
|
v-model="selectedUser.status"
|
|
true-value="active"
|
|
false-value="inactive"
|
|
color="green"
|
|
label="User Account Active"
|
|
hide-details
|
|
></v-switch>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
|
|
<v-card-actions class="pa-4">
|
|
<v-spacer></v-spacer>
|
|
<v-btn variant="text" @click="editDialog = false">
|
|
Cancel
|
|
</v-btn>
|
|
<v-btn color="primary" @click="saveUserChanges">
|
|
<v-icon icon="mdi-content-save" class="mr-2"></v-icon>
|
|
Save Changes
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</v-app>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAuthStore } from '~/stores/auth'
|
|
import { useRBAC } from '~/composables/useRBAC'
|
|
|
|
const router = useRouter()
|
|
const authStore = useAuthStore()
|
|
const { hasRole, hasRank } = useRBAC()
|
|
|
|
// State
|
|
const drawer = ref(false)
|
|
const search = ref('')
|
|
const users = ref<any[]>([])
|
|
const editDialog = ref(false)
|
|
const selectedUser = ref<any>(null)
|
|
const selectedGeographicalScope = ref<string>('Global')
|
|
const moderationTab = ref<string>('users')
|
|
|
|
// Geographical scopes for filtering
|
|
const geographicalScopes = ref([
|
|
{ value: 'Global', label: 'Global', icon: 'mdi-earth', description: 'All users worldwide' },
|
|
{ value: 'Hungary', label: 'Hungary', icon: 'mdi-flag', description: 'Users in Hungary' },
|
|
{ value: 'Pest County', label: 'Pest County', icon: 'mdi-map-marker-radius', description: 'Users in Pest County' },
|
|
{ value: 'Budapest', label: 'Budapest', icon: 'mdi-city', description: 'Users in Budapest' },
|
|
{ value: 'District V', label: 'District V', icon: 'mdi-map-marker', description: 'Users in District V' },
|
|
])
|
|
|
|
// Data Table Headers
|
|
const headers = ref([
|
|
{ title: 'Email', key: 'email', sortable: true },
|
|
{ title: 'Current Role', key: 'role', sortable: true },
|
|
{ title: 'Scope Level', key: 'scope_level', sortable: true },
|
|
{ title: 'Region/Country', key: 'region_display', sortable: true },
|
|
{ title: 'Status', key: 'status', sortable: true },
|
|
{ title: 'Created', key: 'created_at', sortable: true },
|
|
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' },
|
|
])
|
|
|
|
// User data from auth store
|
|
const userEmail = computed(() => authStore.getUserEmail || 'Unknown')
|
|
const userRole = computed(() => authStore.getUserRole || 'Unknown')
|
|
const userRank = computed(() => authStore.getUserRank || 0)
|
|
const scopeLevel = computed(() => authStore.getScopeLevel || 'Global')
|
|
const scopeId = computed(() => authStore.getScopeId || 0)
|
|
|
|
// Role color mapping
|
|
const roleColor = computed(() => {
|
|
switch (userRole.value) {
|
|
case 'superadmin': return 'red'
|
|
case 'admin': return 'orange'
|
|
case 'moderator': return 'blue'
|
|
case 'sales_agent': return 'green'
|
|
default: return 'grey'
|
|
}
|
|
})
|
|
|
|
// Helper function to check if user matches geographical scope
|
|
const userMatchesGeographicalScope = (user: any): boolean => {
|
|
if (selectedGeographicalScope.value === 'Global') return true
|
|
|
|
switch (selectedGeographicalScope.value) {
|
|
case 'Hungary':
|
|
return user.country_code === 'HU' || user.scope_level === 'Global'
|
|
case 'Pest County':
|
|
return user.region_code === 'HU-PE' ||
|
|
user.country_code === 'HU' ||
|
|
user.scope_level === 'Global'
|
|
case 'Budapest':
|
|
return user.region_code === 'HU-BU' ||
|
|
user.region_code === 'HU-PE' ||
|
|
user.country_code === 'HU' ||
|
|
user.scope_level === 'Global'
|
|
case 'District V':
|
|
return user.region_code === 'HU-BU-05' ||
|
|
user.region_code === 'HU-BU' ||
|
|
user.region_code === 'HU-PE' ||
|
|
user.country_code === 'HU' ||
|
|
user.scope_level === 'Global'
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Helper function to get region/country display string
|
|
const getRegionDisplay = (user: any): string => {
|
|
if (user.scope_level === 'Global') return 'Global'
|
|
if (user.region_code && user.country_code) return `${user.region_code} (${user.country_code})`
|
|
if (user.country_code) return user.country_code
|
|
return 'N/A'
|
|
}
|
|
|
|
// Helper function to get scope display name
|
|
const getScopeDisplayName = (scope: string): string => {
|
|
const found = geographicalScopes.value.find(s => s.value === scope)
|
|
return found ? found.label : 'All Scopes'
|
|
}
|
|
|
|
// Filtered users based on search and geographical scope
|
|
const filteredUsers = computed(() => {
|
|
let filtered = users.value
|
|
|
|
// Apply geographical scope filter
|
|
if (selectedGeographicalScope.value && selectedGeographicalScope.value !== 'Global') {
|
|
filtered = filtered.filter(userMatchesGeographicalScope)
|
|
}
|
|
|
|
// Apply search filter
|
|
if (search.value) {
|
|
const query = search.value.toLowerCase()
|
|
filtered = filtered.filter(user =>
|
|
user.email.toLowerCase().includes(query) ||
|
|
user.role.toLowerCase().includes(query) ||
|
|
user.scope_level.toLowerCase().includes(query) ||
|
|
(user.country_code && user.country_code.toLowerCase().includes(query)) ||
|
|
(user.region_code && user.region_code.toLowerCase().includes(query))
|
|
)
|
|
}
|
|
|
|
return filtered
|
|
})
|
|
|
|
// Role hierarchy for display
|
|
const roleHierarchy = ref([
|
|
{ name: 'Superadmin', rank: 10, icon: 'mdi-crown', color: 'red', description: 'Full system access', scope: 'Global only' },
|
|
{ name: 'Admin', rank: 7, icon: 'mdi-shield-account', color: 'orange', description: 'Administrative access', scope: 'Country+' },
|
|
{ name: 'Moderator', rank: 5, icon: 'mdi-account-tie', color: 'blue', description: 'Content moderation', scope: 'Region+' },
|
|
{ name: 'Sales Agent', rank: 3, icon: 'mdi-account-cash', color: 'green', description: 'Sales and customer service', scope: 'City+' },
|
|
])
|
|
|
|
// Scope levels for display and selection
|
|
const scopeLevels = ref([
|
|
{ level: 'Global', icon: 'mdi-earth', color: 'purple', description: 'Access to all data worldwide', example: 'World' },
|
|
{ level: 'Country', icon: 'mdi-flag', color: 'blue', description: 'Access within a specific country', example: 'Hungary' },
|
|
{ level: 'Region', icon: 'mdi-map-marker-radius', color: 'green', description: 'Access within a region', example: 'Central Hungary' },
|
|
{ level: 'City', icon: 'mdi-city', color: 'orange', description: 'Access within a city', example: 'Budapest' },
|
|
{ level: 'District', icon: 'mdi-map-marker', color: 'red', description: 'Access within a district', example: 'District V' },
|
|
])
|
|
|
|
// Available countries for selection
|
|
const availableCountries = ref([
|
|
{ code: 'HU', name: 'Hungary', icon: 'mdi-flag' },
|
|
{ code: 'DE', name: 'Germany', icon: 'mdi-flag' },
|
|
{ code: 'GB', name: 'United Kingdom', icon: 'mdi-flag' },
|
|
{ code: 'FR', name: 'France', icon: 'mdi-flag' },
|
|
{ code: 'IT', name: 'Italy', icon: 'mdi-flag' },
|
|
{ code: 'US', name: 'United States', icon: 'mdi-flag' },
|
|
])
|
|
|
|
// Available regions based on country
|
|
const availableRegions = computed(() => {
|
|
if (!selectedUser.value?.country_code) return []
|
|
|
|
const regionsByCountry: Record<string, Array<{code: string, name: string}>> = {
|
|
'HU': [
|
|
{ code: 'HU-BU', name: 'Budapest' },
|
|
{ code: 'HU-PE', name: 'Pest County' },
|
|
{ code: 'HU-BA', name: 'Baranya' },
|
|
{ code: 'HU-BZ', name: 'Borsod-Abaúj-Zemplén' },
|
|
],
|
|
'DE': [
|
|
{ code: 'DE-BE', name: 'Berlin' },
|
|
{ code: 'DE-BY', name: 'Bavaria' },
|
|
{ code: 'DE-HH', name: 'Hamburg' },
|
|
],
|
|
'GB': [
|
|
{ code: 'GB-LON', name: 'London' },
|
|
{ code: 'GB-MAN', name: 'Manchester' },
|
|
{ code: 'GB-BIR', name: 'Birmingham' },
|
|
],
|
|
}
|
|
|
|
return regionsByCountry[selectedUser.value.country_code] || []
|
|
})
|
|
|
|
// Check if geographical scope picker should be shown
|
|
const showGeographicalScopePicker = computed(() => {
|
|
if (!selectedUser.value) return false
|
|
|
|
// Show for regionally restricted roles (Admin, Moderator, Sales Agent) when scope level is not Global
|
|
const regionallyRestrictedRoles = ['admin', 'moderator', 'sales_agent']
|
|
const isRegionallyRestricted = regionallyRestrictedRoles.includes(selectedUser.value.role)
|
|
const isNotGlobal = selectedUser.value.scope_level !== 'Global'
|
|
|
|
return isRegionallyRestricted && isNotGlobal
|
|
})
|
|
|
|
// Helper function to get country icon
|
|
const getCountryIcon = (countryCode: string) => {
|
|
return 'mdi-flag'
|
|
}
|
|
|
|
// Navigation
|
|
const navigateTo = (path: string) => {
|
|
router.push(path)
|
|
}
|
|
|
|
const logout = () => {
|
|
authStore.logout()
|
|
router.push('/login')
|
|
}
|
|
|
|
// Helper functions
|
|
const getRoleColor = (role: string) => {
|
|
switch (role) {
|
|
case 'superadmin': return 'red'
|
|
case 'admin': return 'orange'
|
|
case 'moderator': return 'blue'
|
|
case 'sales_agent': return 'green'
|
|
default: return 'grey'
|
|
}
|
|
}
|
|
|
|
const getRoleIcon = (role: string) => {
|
|
switch (role) {
|
|
case 'superadmin': return 'mdi-crown'
|
|
case 'admin': return 'mdi-shield-account'
|
|
case 'moderator': return 'mdi-account-tie'
|
|
case 'sales_agent': return 'mdi-account-cash'
|
|
default: return 'mdi-account'
|
|
}
|
|
}
|
|
|
|
const formatRole = (role: string) => {
|
|
return role.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
|
}
|
|
|
|
const getScopeColor = (scope: string) => {
|
|
switch (scope) {
|
|
case 'Global': return 'purple'
|
|
case 'Country': return 'blue'
|
|
case 'Region': return 'green'
|
|
case 'City': return 'orange'
|
|
case 'District': return 'red'
|
|
default: return 'grey'
|
|
}
|
|
}
|
|
|
|
const getScopeIcon = (scope: string) => {
|
|
switch (scope) {
|
|
case 'Global': return 'mdi-earth'
|
|
case 'Country': return 'mdi-flag'
|
|
case 'Region': return 'mdi-map-marker-radius'
|
|
case 'City': return 'mdi-city'
|
|
case 'District': return 'mdi-map-marker'
|
|
default: return 'mdi-map'
|
|
}
|
|
}
|
|
|
|
const canEditUser = (user: any) => {
|
|
// Superadmin can edit anyone
|
|
if (hasRole('superadmin')) return true
|
|
|
|
// Admin cannot edit superadmin or other admins with higher rank
|
|
if (hasRole('admin')) {
|
|
if (user.role === 'superadmin') return false
|
|
if (user.role === 'admin' && user.id !== authStore.getUserId) return false
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
const openEditDialog = (user: any) => {
|
|
selectedUser.value = { ...user }
|
|
editDialog.value = true
|
|
}
|
|
|
|
const toggleUserStatus = (user: any) => {
|
|
const index = users.value.findIndex(u => u.id === user.id)
|
|
if (index !== -1) {
|
|
users.value[index].status = user.status === 'active' ? 'inactive' : 'active'
|
|
}
|
|
}
|
|
|
|
const viewUserDetails = (user: any) => {
|
|
// In a real implementation, this would navigate to user details page
|
|
console.log('View user details:', user)
|
|
// For now, show a snackbar notification
|
|
// You would implement a proper details view
|
|
}
|
|
|
|
const saveUserChanges = () => {
|
|
if (!selectedUser.value) return
|
|
|
|
// Find the user in the list and update
|
|
const index = users.value.findIndex(u => u.id === selectedUser.value.id)
|
|
if (index !== -1) {
|
|
// Update the user with new values
|
|
users.value[index] = { ...selectedUser.value }
|
|
|
|
// In a real implementation, this would call an API endpoint
|
|
console.log('Saving user changes:', selectedUser.value)
|
|
|
|
// Show success message (in a real app, you'd use a snackbar/toast)
|
|
alert(`User ${selectedUser.value.email} updated successfully!`)
|
|
}
|
|
|
|
// Close the dialog
|
|
editDialog.value = false
|
|
selectedUser.value = null
|
|
}
|
|
|
|
// Moderation Queue Data
|
|
const pendingUsersHeaders = ref([
|
|
{ title: 'User', key: 'user', sortable: true },
|
|
{ title: 'Requested Role', key: 'requested_role', sortable: true },
|
|
{ title: 'Scope', key: 'scope', sortable: true },
|
|
{ title: 'Requested At', key: 'requested_at', sortable: true },
|
|
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' },
|
|
])
|
|
|
|
const pendingServicesHeaders = ref([
|
|
{ title: 'Service', key: 'service', sortable: true },
|
|
{ title: 'Category', key: 'category', sortable: true },
|
|
{ title: 'Provider', key: 'provider', sortable: true },
|
|
{ title: 'Requested At', key: 'requested_at', sortable: true },
|
|
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' },
|
|
])
|
|
|
|
// Mock pending approvals data
|
|
const pendingUsers = ref([
|
|
{ id: 101, email: 'new.user@example.com', requested_role: 'sales_agent', scope_level: 'City', country_code: 'HU', region_code: 'HU-BU', requested_at: '2026-03-22' },
|
|
{ id: 102, email: 'partner.admin@partner.com', requested_role: 'admin', scope_level: 'Country', country_code: 'DE', region_code: undefined, requested_at: '2026-03-21' },
|
|
{ id: 103, email: 'local.moderator@example.hu', requested_role: 'moderator', scope_level: 'Region', country_code: 'HU', region_code: 'HU-PE', requested_at: '2026-03-20' },
|
|
{ id: 104, email: 'district.agent@example.com', requested_role: 'sales_agent', scope_level: 'District', country_code: 'HU', region_code: 'HU-BU-05', requested_at: '2026-03-19' },
|
|
])
|
|
|
|
const pendingServices = ref([
|
|
{ id: 201, name: 'Premium Car Wash', category: 'Cleaning', provider: 'CleanCars Ltd.', requested_at: '2026-03-22' },
|
|
{ id: 202, name: 'Tire Replacement', category: 'Maintenance', provider: 'TireMaster', requested_at: '2026-03-21' },
|
|
{ id: 203, name: 'Oil Change Service', category: 'Maintenance', provider: 'QuickOil', requested_at: '2026-03-20' },
|
|
])
|
|
|
|
// Computed pending approvals filtered by geographical scope
|
|
const pendingApprovals = computed(() => {
|
|
// In a real implementation, this would filter by selectedGeographicalScope
|
|
return [...pendingUsers.value, ...pendingServices.value]
|
|
})
|
|
|
|
// Approval actions
|
|
const approvePendingItem = (item: any, type: string) => {
|
|
console.log(`Approving ${type}:`, item)
|
|
// In a real implementation, this would call an API endpoint
|
|
|
|
// Remove from pending list
|
|
if (type === 'user') {
|
|
const index = pendingUsers.value.findIndex(u => u.id === item.id)
|
|
if (index !== -1) {
|
|
pendingUsers.value.splice(index, 1)
|
|
}
|
|
} else if (type === 'service') {
|
|
const index = pendingServices.value.findIndex(s => s.id === item.id)
|
|
if (index !== -1) {
|
|
pendingServices.value.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
alert(`${type.charAt(0).toUpperCase() + type.slice(1)} "${item.email || item.name}" approved successfully!`)
|
|
}
|
|
|
|
const rejectPendingItem = (item: any, type: string) => {
|
|
console.log(`Rejecting ${type}:`, item)
|
|
// In a real implementation, this would call an API endpoint
|
|
|
|
// Remove from pending list
|
|
if (type === 'user') {
|
|
const index = pendingUsers.value.findIndex(u => u.id === item.id)
|
|
if (index !== -1) {
|
|
pendingUsers.value.splice(index, 1)
|
|
}
|
|
} else if (type === 'service') {
|
|
const index = pendingServices.value.findIndex(s => s.id === item.id)
|
|
if (index !== -1) {
|
|
pendingServices.value.splice(index, 1)
|
|
}
|
|
}
|
|
|
|
alert(`${type.charAt(0).toUpperCase() + type.slice(1)} "${item.email || item.name}" rejected.`)
|
|
}
|
|
|
|
// Mock data loading for testing
|
|
const loadMockData = () => {
|
|
users.value = [
|
|
{ id: 1, email: 'superadmin@servicefinder.com', role: 'superadmin', scope_level: 'Global', status: 'active', created_at: '2026-01-15', country_code: undefined, region_code: undefined },
|
|
{ id: 2, email: 'admin.hu@servicefinder.com', role: 'admin', scope_level: 'Country', status: 'active', created_at: '2026-02-10', country_code: 'HU', region_code: undefined },
|
|
{ id: 3, email: 'moderator.pest@servicefinder.com', role: 'moderator', scope_level: 'Region', status: 'active', created_at: '2026-02-20', country_code: 'HU', region_code: 'HU-PE' },
|
|
{ id: 4, email: 'sales.budapest@servicefinder.com', role: 'sales_agent', scope_level: 'City', status: 'active', created_at: '2026-03-01', country_code: 'HU', region_code: 'HU-BU' },
|
|
{ id: 5, email: 'agent.district5@servicefinder.com', role: 'sales_agent', scope_level: 'District', status: 'inactive', created_at: '2026-03-05', country_code: 'HU', region_code: 'HU-BU-05' },
|
|
{ id: 6, email: 'admin.de@servicefinder.com', role: 'admin', scope_level: 'Country', status: 'active', created_at: '2026-03-10', country_code: 'DE', region_code: undefined },
|
|
{ id: 7, email: 'moderator.berlin@servicefinder.com', role: 'moderator', scope_level: 'City', status: 'active', created_at: '2026-03-12', country_code: 'DE', region_code: 'DE-BE' },
|
|
{ id: 8, email: 'sales.london@servicefinder.com', role: 'sales_agent', scope_level: 'City', status: 'active', created_at: '2026-03-15', country_code: 'GB', region_code: 'GB-LON' },
|
|
{ id: 9, email: 'john.doe@example.com', role: 'sales_agent', scope_level: 'Region', status: 'active', created_at: '2026-03-16', country_code: 'HU', region_code: 'HU-PE' },
|
|
{ id: 10, email: 'jane.smith@partner.com', role: 'moderator', scope_level: 'Country', status: 'active', created_at: '2026-03-17', country_code: 'GB', region_code: undefined },
|
|
]
|
|
}
|
|
|
|
// Check RBAC permissions on mount
|
|
onMounted(() => {
|
|
// Only superadmin and admin can access this page
|
|
if (!hasRole('superadmin') && !hasRole('admin')) {
|
|
console.warn('Access denied: User does not have required role (superadmin or admin)')
|
|
navigateTo('/unauthorized')
|
|
return
|
|
}
|
|
|
|
// Load initial mock data
|
|
loadMockData()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.v-card {
|
|
border-radius: 12px;
|
|
}
|
|
.v-list-item {
|
|
border-radius: 8px;
|
|
margin-bottom: 4px;
|
|
}
|
|
</style> |