Files
2026-03-23 21:43:40 +00:00

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>