admin firs step

This commit is contained in:
Roo
2026-03-23 21:43:40 +00:00
parent 309a72cc0b
commit cddcd34ba9
47 changed files with 22698 additions and 19 deletions

View File

@@ -0,0 +1,335 @@
import { ref, computed } from 'vue'
import { useAuthStore } from '~/stores/auth'
// Types
export interface HealthMetrics {
total_assets: number
total_organizations: number
critical_alerts_24h: number
system_status: 'healthy' | 'degraded' | 'critical'
uptime_percentage: number
response_time_ms: number
database_connections: number
active_users: number
last_updated: string
}
export interface SystemAlert {
id: string
severity: 'info' | 'warning' | 'critical'
title: string
description: string
timestamp: string
component: string
resolved: boolean
}
export interface HealthMonitorState {
metrics: HealthMetrics | null
alerts: SystemAlert[]
loading: boolean
error: string | null
lastUpdated: Date | null
}
// Mock data for development/testing
const generateMockMetrics = (): HealthMetrics => {
return {
total_assets: Math.floor(Math.random() * 10000) + 5000,
total_organizations: Math.floor(Math.random() * 500) + 100,
critical_alerts_24h: Math.floor(Math.random() * 10),
system_status: Math.random() > 0.8 ? 'degraded' : Math.random() > 0.95 ? 'critical' : 'healthy',
uptime_percentage: 99.5 + (Math.random() * 0.5 - 0.25), // 99.25% - 99.75%
response_time_ms: Math.floor(Math.random() * 100) + 50,
database_connections: Math.floor(Math.random() * 50) + 10,
active_users: Math.floor(Math.random() * 1000) + 500,
last_updated: new Date().toISOString()
}
}
const generateMockAlerts = (count: number = 5): SystemAlert[] => {
const severities: SystemAlert['severity'][] = ['info', 'warning', 'critical']
const components = ['Database', 'API Gateway', 'Redis', 'PostgreSQL', 'Docker', 'Network', 'Authentication', 'File Storage']
const titles = [
'High memory usage detected',
'Database connection pool exhausted',
'API response time above threshold',
'Redis cache miss rate increased',
'Disk space running low',
'Network latency spike',
'Authentication service slow response',
'Backup job failed'
]
const alerts: SystemAlert[] = []
for (let i = 0; i < count; i++) {
const severity = severities[Math.floor(Math.random() * severities.length)]
const isResolved = Math.random() > 0.7
alerts.push({
id: `alert_${Date.now()}_${i}`,
severity,
title: titles[Math.floor(Math.random() * titles.length)],
description: `Detailed description of the ${severity} alert in the ${components[Math.floor(Math.random() * components.length)]} component.`,
timestamp: new Date(Date.now() - Math.random() * 24 * 60 * 60 * 1000).toISOString(), // Within last 24 hours
component: components[Math.floor(Math.random() * components.length)],
resolved: isResolved
})
}
return alerts
}
// API Service
class HealthMonitorApiService {
private baseUrl = 'http://localhost:8000/api/v1/admin' // Should come from environment config
private delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// Get health metrics
async getHealthMetrics(): Promise<HealthMetrics> {
// In a real implementation, this would call the actual API
// const response = await fetch(`${this.baseUrl}/health-monitor`, {
// headers: this.getAuthHeaders()
// })
//
// if (!response.ok) {
// throw new Error(`HTTP ${response.status}: ${response.statusText}`)
// }
//
// return await response.json()
await this.delay(800) // Simulate network delay
// For now, return mock data
return generateMockMetrics()
}
// Get system alerts
async getSystemAlerts(options?: {
severity?: SystemAlert['severity']
resolved?: boolean
limit?: number
}): Promise<SystemAlert[]> {
await this.delay(500)
let alerts = generateMockAlerts(10)
if (options?.severity) {
alerts = alerts.filter(alert => alert.severity === options.severity)
}
if (options?.resolved !== undefined) {
alerts = alerts.filter(alert => alert.resolved === options.resolved)
}
if (options?.limit) {
alerts = alerts.slice(0, options.limit)
}
return alerts
}
// Get auth headers (for real API calls)
private getAuthHeaders(): Record<string, string> {
const authStore = useAuthStore()
const headers: Record<string, string> = {
'Content-Type': 'application/json'
}
if (authStore.token) {
headers['Authorization'] = `Bearer ${authStore.token}`
}
// Add geographical scope headers
if (authStore.getScopeId) {
headers['X-Scope-Id'] = authStore.getScopeId.toString()
}
if (authStore.getRegionCode) {
headers['X-Region-Code'] = authStore.getRegionCode
}
if (authStore.getScopeLevel) {
headers['X-Scope-Level'] = authStore.getScopeLevel
}
return headers
}
}
// Composable
export const useHealthMonitor = () => {
const state = ref<HealthMonitorState>({
metrics: null,
alerts: [],
loading: false,
error: null,
lastUpdated: null
})
const apiService = new HealthMonitorApiService()
// Computed properties
const systemStatusColor = computed(() => {
if (!state.value.metrics) return 'grey'
switch (state.value.metrics.system_status) {
case 'healthy': return 'green'
case 'degraded': return 'orange'
case 'critical': return 'red'
default: return 'grey'
}
})
const systemStatusIcon = computed(() => {
if (!state.value.metrics) return 'mdi-help-circle'
switch (state.value.metrics.system_status) {
case 'healthy': return 'mdi-check-circle'
case 'degraded': return 'mdi-alert-circle'
case 'critical': return 'mdi-alert-octagon'
default: return 'mdi-help-circle'
}
})
const criticalAlerts = computed(() => {
return state.value.alerts.filter(alert => alert.severity === 'critical' && !alert.resolved)
})
const warningAlerts = computed(() => {
return state.value.alerts.filter(alert => alert.severity === 'warning' && !alert.resolved)
})
const formattedUptime = computed(() => {
if (!state.value.metrics) return 'N/A'
return `${state.value.metrics.uptime_percentage.toFixed(2)}%`
})
const formattedResponseTime = computed(() => {
if (!state.value.metrics) return 'N/A'
return `${state.value.metrics.response_time_ms}ms`
})
// Actions
const fetchHealthMetrics = async () => {
state.value.loading = true
state.value.error = null
try {
const metrics = await apiService.getHealthMetrics()
state.value.metrics = metrics
state.value.lastUpdated = new Date()
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to fetch health metrics'
console.error('Error fetching health metrics:', error)
// Fallback to mock data
state.value.metrics = generateMockMetrics()
} finally {
state.value.loading = false
}
}
const fetchSystemAlerts = async (options?: {
severity?: SystemAlert['severity']
resolved?: boolean
limit?: number
}) => {
state.value.loading = true
state.value.error = null
try {
const alerts = await apiService.getSystemAlerts(options)
state.value.alerts = alerts
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to fetch system alerts'
console.error('Error fetching system alerts:', error)
// Fallback to mock data
state.value.alerts = generateMockAlerts(5)
} finally {
state.value.loading = false
}
}
const refreshAll = async () => {
await Promise.all([
fetchHealthMetrics(),
fetchSystemAlerts()
])
}
const markAlertAsResolved = async (alertId: string) => {
// In a real implementation, this would call an API endpoint
// await apiService.resolveAlert(alertId)
// Update local state
const alertIndex = state.value.alerts.findIndex(alert => alert.id === alertId)
if (alertIndex !== -1) {
state.value.alerts[alertIndex].resolved = true
}
}
const dismissAlert = (alertId: string) => {
// Remove alert from local state (frontend only)
state.value.alerts = state.value.alerts.filter(alert => alert.id !== alertId)
}
// Initialize
const initialize = () => {
refreshAll()
}
return {
// State
state: computed(() => state.value),
metrics: computed(() => state.value.metrics),
alerts: computed(() => state.value.alerts),
loading: computed(() => state.value.loading),
error: computed(() => state.value.error),
lastUpdated: computed(() => state.value.lastUpdated),
// Computed
systemStatusColor,
systemStatusIcon,
criticalAlerts,
warningAlerts,
formattedUptime,
formattedResponseTime,
// Actions
fetchHealthMetrics,
fetchSystemAlerts,
refreshAll,
markAlertAsResolved,
dismissAlert,
initialize,
// Helper functions
getAlertColor: (severity: SystemAlert['severity']) => {
switch (severity) {
case 'info': return 'blue'
case 'warning': return 'orange'
case 'critical': return 'red'
default: return 'grey'
}
},
getAlertIcon: (severity: SystemAlert['severity']) => {
switch (severity) {
case 'info': return 'mdi-information'
case 'warning': return 'mdi-alert'
case 'critical': return 'mdi-alert-circle'
default: return 'mdi-help-circle'
}
},
formatTimestamp: (timestamp: string) => {
const date = new Date(timestamp)
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
}
}
export default useHealthMonitor

View File

@@ -0,0 +1,200 @@
import { ref, onMounted, onUnmounted } from 'vue'
export interface PollingOptions {
interval?: number // milliseconds
immediate?: boolean // whether to execute immediately on start
maxRetries?: number // maximum number of retries on error
retryDelay?: number // delay between retries in milliseconds
onError?: (error: Error) => void // error handler
}
export interface PollingState {
isPolling: boolean
isFetching: boolean
error: string | null
retryCount: number
lastFetchTime: Date | null
}
/**
* Composable for implementing polling/real-time updates
*
* @param callback - Function to execute on each poll
* @param options - Polling configuration options
* @returns Polling controls and state
*/
export const usePolling = <T>(
callback: () => Promise<T> | T,
options: PollingOptions = {}
) => {
const {
interval = 3000, // 3 seconds default
immediate = true,
maxRetries = 3,
retryDelay = 1000,
onError
} = options
// State
const state = ref<PollingState>({
isPolling: false,
isFetching: false,
error: null,
retryCount: 0,
lastFetchTime: null
})
// Polling interval reference
let pollInterval: NodeJS.Timeout | null = null
let retryTimeout: NodeJS.Timeout | null = null
// Execute the polling callback
const executePoll = async (): Promise<T | null> => {
if (state.value.isFetching) {
return null // Skip if already fetching
}
state.value.isFetching = true
state.value.error = null
try {
const result = await callback()
state.value.lastFetchTime = new Date()
state.value.retryCount = 0 // Reset retry count on success
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
state.value.error = errorMessage
state.value.retryCount++
// Call error handler if provided
if (onError) {
onError(error instanceof Error ? error : new Error(errorMessage))
}
// Handle retries
if (state.value.retryCount <= maxRetries) {
console.warn(`Polling error (retry ${state.value.retryCount}/${maxRetries}):`, errorMessage)
// Schedule retry
if (retryTimeout) {
clearTimeout(retryTimeout)
}
retryTimeout = setTimeout(() => {
executePoll()
}, retryDelay)
} else {
console.error(`Polling failed after ${maxRetries} retries:`, errorMessage)
stopPolling() // Stop polling after max retries
}
return null
} finally {
state.value.isFetching = false
}
}
// Start polling
const startPolling = () => {
if (state.value.isPolling) {
return // Already polling
}
state.value.isPolling = true
state.value.error = null
// Execute immediately if requested
if (immediate) {
executePoll()
}
// Set up interval
pollInterval = setInterval(() => {
executePoll()
}, interval)
}
// Stop polling
const stopPolling = () => {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
if (retryTimeout) {
clearTimeout(retryTimeout)
retryTimeout = null
}
state.value.isPolling = false
state.value.isFetching = false
}
// Toggle polling
const togglePolling = () => {
if (state.value.isPolling) {
stopPolling()
} else {
startPolling()
}
}
// Force immediate execution
const forcePoll = async (): Promise<T | null> => {
return await executePoll()
}
// Update polling interval
const updateInterval = (newInterval: number) => {
const wasPolling = state.value.isPolling
if (wasPolling) {
stopPolling()
}
// Update interval in options (for next start)
options.interval = newInterval
if (wasPolling) {
startPolling()
}
}
// Cleanup on unmount
onUnmounted(() => {
stopPolling()
})
// Auto-start on mount if immediate is true
onMounted(() => {
if (immediate) {
startPolling()
}
})
return {
// State
state: state.value,
isPolling: state.value.isPolling,
isFetching: state.value.isFetching,
error: state.value.error,
retryCount: state.value.retryCount,
lastFetchTime: state.value.lastFetchTime,
// Controls
startPolling,
stopPolling,
togglePolling,
forcePoll,
updateInterval,
// Helper
resetError: () => {
state.value.error = null
state.value.retryCount = 0
}
}
}
export default usePolling

View File

@@ -0,0 +1,237 @@
import { useAuthStore } from '~/stores/auth'
// Role definitions with hierarchical ranks
export enum Role {
SUPERADMIN = 'superadmin',
ADMIN = 'admin',
MODERATOR = 'moderator',
SALESPERSON = 'salesperson'
}
// Scope level definitions
export enum ScopeLevel {
GLOBAL = 'global',
COUNTRY = 'country',
REGION = 'region',
CITY = 'city',
DISTRICT = 'district'
}
// Role rank mapping (higher number = higher authority)
export const RoleRank: Record<Role, number> = {
[Role.SUPERADMIN]: 10,
[Role.ADMIN]: 7,
[Role.MODERATOR]: 5,
[Role.SALESPERSON]: 3
}
// Tile permissions mapping
export interface TilePermission {
id: string
title: string
description: string
requiredRole: Role[]
minRank?: number
requiredPermission?: string
scopeLevel?: ScopeLevel[]
}
// Available tiles with RBAC requirements
export const AdminTiles: TilePermission[] = [
{
id: 'ai-logs',
title: 'AI Logs Monitor',
description: 'Real-time tracking of AI robot pipelines',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.MODERATOR],
minRank: 5,
requiredPermission: 'view:dashboard'
},
{
id: 'financial-dashboard',
title: 'Financial Dashboard',
description: 'Revenue, expenses, ROI metrics with geographical filtering',
requiredRole: [Role.SUPERADMIN, Role.ADMIN],
minRank: 7,
requiredPermission: 'view:finance',
scopeLevel: [ScopeLevel.GLOBAL, ScopeLevel.COUNTRY, ScopeLevel.REGION]
},
{
id: 'salesperson-hub',
title: 'Salesperson Hub',
description: 'Performance metrics, leads, conversions for sales teams',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.SALESPERSON],
minRank: 3,
requiredPermission: 'view:sales'
},
{
id: 'user-management',
title: 'User Management',
description: 'Active users, registration trends, moderation queue',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.MODERATOR],
minRank: 5,
requiredPermission: 'view:users',
scopeLevel: [ScopeLevel.GLOBAL, ScopeLevel.COUNTRY, ScopeLevel.REGION, ScopeLevel.CITY]
},
{
id: 'service-moderation-map',
title: 'Service Moderation Map',
description: 'Geographical view of pending/flagged services',
requiredRole: [Role.SUPERADMIN, Role.ADMIN, Role.MODERATOR],
minRank: 5,
requiredPermission: 'moderate:services',
scopeLevel: [ScopeLevel.CITY, ScopeLevel.DISTRICT]
},
{
id: 'gamification-control',
title: 'Gamification Control',
description: 'XP levels, badges, penalty system administration',
requiredRole: [Role.SUPERADMIN, Role.ADMIN],
minRank: 7,
requiredPermission: 'manage:settings'
},
{
id: 'system-health',
title: 'System Health',
description: 'API status, database metrics, uptime monitoring',
requiredRole: [Role.SUPERADMIN, Role.ADMIN],
minRank: 7,
requiredPermission: 'view:dashboard'
}
]
// Composable for RBAC checks
export function useRBAC() {
const authStore = useAuthStore()
// Check if user can access a specific tile
function canAccessTile(tileId: string): boolean {
const tile = AdminTiles.find(t => t.id === tileId)
if (!tile) return false
// Check role
if (!tile.requiredRole.includes(authStore.getUserRole as Role)) {
return false
}
// Check rank
if (tile.minRank && !authStore.hasRank(tile.minRank)) {
return false
}
// Check permission
if (tile.requiredPermission && !authStore.hasPermission(tile.requiredPermission)) {
return false
}
// Check scope level
if (tile.scopeLevel && tile.scopeLevel.length > 0) {
const userScopeLevel = authStore.getScopeLevel as ScopeLevel
if (!tile.scopeLevel.includes(userScopeLevel)) {
return false
}
}
return true
}
// Get filtered tiles for current user
function getFilteredTiles(): TilePermission[] {
return AdminTiles.filter(tile => canAccessTile(tile.id))
}
// Check if user can perform action
function canPerformAction(permission: string, minRank?: number): boolean {
if (!authStore.hasPermission(permission)) {
return false
}
if (minRank && !authStore.hasRank(minRank)) {
return false
}
return true
}
// Check if user can access scope
function canAccessScope(scopeLevel: ScopeLevel, scopeId?: number, regionCode?: string): boolean {
const userScopeLevel = authStore.getScopeLevel as ScopeLevel
// Superadmin can access everything
if (authStore.getUserRole === Role.SUPERADMIN) {
return true
}
// Check scope level hierarchy
const scopeHierarchy = [
ScopeLevel.GLOBAL,
ScopeLevel.COUNTRY,
ScopeLevel.REGION,
ScopeLevel.CITY,
ScopeLevel.DISTRICT
]
const userLevelIndex = scopeHierarchy.indexOf(userScopeLevel)
const requestedLevelIndex = scopeHierarchy.indexOf(scopeLevel)
// User can only access their level or lower (more specific) levels
if (requestedLevelIndex < userLevelIndex) {
return false
}
// Check specific scope ID or region code if provided
if (scopeId || regionCode) {
return authStore.canAccessScope(scopeId || 0, regionCode)
}
return true
}
// Get user's accessible scope levels
function getAccessibleScopeLevels(): ScopeLevel[] {
const userScopeLevel = authStore.getScopeLevel as ScopeLevel
const scopeHierarchy = [
ScopeLevel.GLOBAL,
ScopeLevel.COUNTRY,
ScopeLevel.REGION,
ScopeLevel.CITY,
ScopeLevel.DISTRICT
]
const userLevelIndex = scopeHierarchy.indexOf(userScopeLevel)
return scopeHierarchy.slice(userLevelIndex)
}
// Get role color for UI
function getRoleColor(role?: string): string {
const userRole = role || authStore.getUserRole
switch (userRole) {
case Role.SUPERADMIN:
return 'purple'
case Role.ADMIN:
return 'blue'
case Role.MODERATOR:
return 'green'
case Role.SALESPERSON:
return 'orange'
default:
return 'gray'
}
}
return {
// Data
Role,
ScopeLevel,
RoleRank,
AdminTiles,
// Functions
canAccessTile,
getFilteredTiles,
canPerformAction,
canAccessScope,
getAccessibleScopeLevels,
getRoleColor
}
}

View File

@@ -0,0 +1,185 @@
import { ref, computed } from 'vue'
export interface Service {
id: number
name: string
lat: number
lng: number
status: 'pending' | 'approved'
address: string
distance: number
category: string
}
export interface Scope {
id: string
label: string
bounds: [[number, number], [number, number]] // SW, NE corners
}
export const useServiceMap = () => {
// Mock services around Budapest
const services = ref<Service[]>([
{
id: 1,
name: 'AutoService Budapest',
lat: 47.6333,
lng: 19.1333,
status: 'pending',
address: 'Budapest, Kossuth Lajos utca 12',
distance: 0.5,
category: 'Car Repair'
},
{
id: 2,
name: 'MOL Station',
lat: 47.6400,
lng: 19.1400,
status: 'approved',
address: 'Budapest, Váci út 45',
distance: 1.2,
category: 'Fuel Station'
},
{
id: 3,
name: 'TireMaster',
lat: 47.6200,
lng: 19.1200,
status: 'pending',
address: 'Budapest, Üllői út 78',
distance: 2.1,
category: 'Tire Service'
},
{
id: 4,
name: 'CarWash Express',
lat: 47.6500,
lng: 19.1500,
status: 'approved',
address: 'Budapest, Róna utca 5',
distance: 3.0,
category: 'Car Wash'
},
{
id: 5,
name: 'BrakeCenter',
lat: 47.6100,
lng: 19.1100,
status: 'pending',
address: 'Budapest, Könyves Kálmán körút 32',
distance: 2.5,
category: 'Brake Service'
},
{
id: 6,
name: 'ElectricCar Service',
lat: 47.6000,
lng: 19.1000,
status: 'pending',
address: 'Budapest, Hungária körút 120',
distance: 4.2,
category: 'EV Charging'
},
{
id: 7,
name: 'OilChange Pro',
lat: 47.6700,
lng: 19.1700,
status: 'approved',
address: 'Budapest, Szentmihályi út 67',
distance: 5.1,
category: 'Oil Change'
},
{
id: 8,
name: 'BodyShop Elite',
lat: 47.5900,
lng: 19.0900,
status: 'pending',
address: 'Budapest, Gyáli út 44',
distance: 5.8,
category: 'Body Repair'
}
])
// Simulated RBAC geographical scope
const currentScope = ref<Scope>({
id: 'pest_county',
label: 'Pest County / Central Hungary',
bounds: [[47.3, 18.9], [47.8, 19.5]]
})
const scopeLabel = computed(() => currentScope.value.label)
const pendingServices = computed(() =>
services.value.filter(s => s.status === 'pending')
)
const approvedServices = computed(() =>
services.value.filter(s => s.status === 'approved')
)
const approveService = (serviceId: number) => {
const service = services.value.find(s => s.id === serviceId)
if (service) {
service.status = 'approved'
console.log(`Service ${serviceId} approved`)
}
}
const addMockService = (service: Omit<Service, 'id'>) => {
const newId = Math.max(...services.value.map(s => s.id)) + 1
services.value.push({
id: newId,
...service
})
}
const filterByScope = (servicesList: Service[]) => {
const [sw, ne] = currentScope.value.bounds
return servicesList.filter(s =>
s.lat >= sw[0] && s.lat <= ne[0] &&
s.lng >= sw[1] && s.lng <= ne[1]
)
}
const servicesInScope = computed(() =>
filterByScope(services.value)
)
const changeScope = (scope: Scope) => {
currentScope.value = scope
}
// Available scopes for simulation
const availableScopes: Scope[] = [
{
id: 'budapest',
label: 'Budapest Only',
bounds: [[47.4, 19.0], [47.6, 19.3]]
},
{
id: 'pest_county',
label: 'Pest County / Central Hungary',
bounds: [[47.3, 18.9], [47.8, 19.5]]
},
{
id: 'hungary',
label: 'Whole Hungary',
bounds: [[45.7, 16.1], [48.6, 22.9]]
}
]
return {
services,
pendingServices,
approvedServices,
scopeLabel,
currentScope,
servicesInScope,
approveService,
addMockService,
changeScope,
availableScopes
}
}

View File

@@ -0,0 +1,498 @@
import { ref, computed } from 'vue'
import { useAuthStore } from '~/stores/auth'
// Types
export interface User {
id: number
email: string
role: 'superadmin' | 'admin' | 'moderator' | 'sales_agent'
scope_level: 'Global' | 'Country' | 'Region' | 'City' | 'District'
status: 'active' | 'inactive'
created_at: string
updated_at?: string
last_login?: string
organization_id?: number
region_code?: string
country_code?: string
}
export interface UpdateUserRoleRequest {
role: User['role']
scope_level: User['scope_level']
scope_id?: number
region_code?: string
country_code?: string
}
export interface UserManagementState {
users: User[]
loading: boolean
error: string | null
}
// Geographical scope definitions for mock data
const geographicalScopes = [
// Hungary hierarchy
{ level: 'Country' as const, code: 'HU', name: 'Hungary', region_code: null },
{ level: 'Region' as const, code: 'HU-PE', name: 'Pest County', country_code: 'HU' },
{ level: 'City' as const, code: 'HU-BU', name: 'Budapest', country_code: 'HU', region_code: 'HU-PE' },
{ level: 'District' as const, code: 'HU-BU-05', name: 'District V', country_code: 'HU', region_code: 'HU-BU' },
// Germany hierarchy
{ level: 'Country' as const, code: 'DE', name: 'Germany', region_code: null },
{ level: 'Region' as const, code: 'DE-BE', name: 'Berlin', country_code: 'DE' },
{ level: 'City' as const, code: 'DE-BER', name: 'Berlin', country_code: 'DE', region_code: 'DE-BE' },
// UK hierarchy
{ level: 'Country' as const, code: 'GB', name: 'United Kingdom', region_code: null },
{ level: 'Region' as const, code: 'GB-LON', name: 'London', country_code: 'GB' },
{ level: 'City' as const, code: 'GB-LND', name: 'London', country_code: 'GB', region_code: 'GB-LON' },
]
// Mock data generator with consistent geographical scopes
const generateMockUsers = (count: number = 25): User[] => {
const roles: User['role'][] = ['superadmin', 'admin', 'moderator', 'sales_agent']
const statuses: User['status'][] = ['active', 'inactive']
const domains = ['servicefinder.com', 'example.com', 'partner.com', 'customer.org']
const firstNames = ['John', 'Jane', 'Robert', 'Emily', 'Michael', 'Sarah', 'David', 'Lisa', 'James', 'Maria']
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez']
const users: User[] = []
// Predefined users with specific geographical scopes for testing
const predefinedUsers: Partial<User>[] = [
// Global superadmin
{ email: 'superadmin@servicefinder.com', role: 'superadmin', scope_level: 'Global', country_code: undefined, region_code: undefined },
// Hungary admin
{ email: 'admin.hu@servicefinder.com', role: 'admin', scope_level: 'Country', country_code: 'HU', region_code: undefined },
// Pest County moderator
{ email: 'moderator.pest@servicefinder.com', role: 'moderator', scope_level: 'Region', country_code: 'HU', region_code: 'HU-PE' },
// Budapest sales agent
{ email: 'sales.budapest@servicefinder.com', role: 'sales_agent', scope_level: 'City', country_code: 'HU', region_code: 'HU-BU' },
// District V sales agent
{ email: 'agent.district5@servicefinder.com', role: 'sales_agent', scope_level: 'District', country_code: 'HU', region_code: 'HU-BU-05' },
// Germany admin
{ email: 'admin.de@servicefinder.com', role: 'admin', scope_level: 'Country', country_code: 'DE', region_code: undefined },
// Berlin moderator
{ email: 'moderator.berlin@servicefinder.com', role: 'moderator', scope_level: 'City', country_code: 'DE', region_code: 'DE-BE' },
// UK admin
{ email: 'admin.uk@servicefinder.com', role: 'admin', scope_level: 'Country', country_code: 'GB', region_code: undefined },
// London sales agent
{ email: 'sales.london@servicefinder.com', role: 'sales_agent', scope_level: 'City', country_code: 'GB', region_code: 'GB-LON' },
]
// Add predefined users
predefinedUsers.forEach((userData, index) => {
users.push({
id: index + 1,
email: userData.email!,
role: userData.role!,
scope_level: userData.scope_level!,
status: 'active',
created_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
updated_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
last_login: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
organization_id: Math.floor(Math.random() * 10) + 1,
country_code: userData.country_code,
region_code: userData.region_code,
})
})
// Generate remaining random users
for (let i = users.length + 1; i <= count; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]
const domain = domains[Math.floor(Math.random() * domains.length)]
const role = roles[Math.floor(Math.random() * roles.length)]
const status = statuses[Math.floor(Math.random() * statuses.length)]
// Select a random geographical scope
const scope = geographicalScopes[Math.floor(Math.random() * geographicalScopes.length)]
users.push({
id: i,
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${domain}`,
role,
scope_level: scope.level,
status,
created_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
updated_at: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
last_login: `2026-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
organization_id: Math.floor(Math.random() * 10) + 1,
country_code: scope.country_code || undefined,
region_code: scope.region_code || undefined,
})
}
return users
}
// API Service (Mock implementation)
class UserManagementApiService {
private mockUsers: User[] = generateMockUsers(15)
private delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// Get all users (with optional filtering)
async getUsers(options?: {
role?: User['role']
scope_level?: User['scope_level']
status?: User['status']
search?: string
country_code?: string
region_code?: string
geographical_scope?: 'Global' | 'Hungary' | 'Pest County' | 'Budapest' | 'District V'
}): Promise<User[]> {
await this.delay(500) // Simulate network delay
let filteredUsers = [...this.mockUsers]
if (options?.role) {
filteredUsers = filteredUsers.filter(user => user.role === options.role)
}
if (options?.scope_level) {
filteredUsers = filteredUsers.filter(user => user.scope_level === options.scope_level)
}
if (options?.status) {
filteredUsers = filteredUsers.filter(user => user.status === options.status)
}
if (options?.country_code) {
filteredUsers = filteredUsers.filter(user =>
user.country_code === options.country_code || user.scope_level === 'Global'
)
}
if (options?.region_code) {
filteredUsers = filteredUsers.filter(user =>
user.region_code === options.region_code ||
user.scope_level === 'Global' ||
(user.scope_level === 'Country' && user.country_code === options.country_code)
)
}
// Geographical scope filtering (simplified for demo)
if (options?.geographical_scope) {
switch (options.geographical_scope) {
case 'Global':
// All users
break
case 'Hungary':
filteredUsers = filteredUsers.filter(user =>
user.country_code === 'HU' || user.scope_level === 'Global'
)
break
case 'Pest County':
filteredUsers = filteredUsers.filter(user =>
user.region_code === 'HU-PE' ||
user.country_code === 'HU' ||
user.scope_level === 'Global'
)
break
case 'Budapest':
filteredUsers = filteredUsers.filter(user =>
user.region_code === 'HU-BU' ||
user.region_code === 'HU-PE' ||
user.country_code === 'HU' ||
user.scope_level === 'Global'
)
break
case 'District V':
filteredUsers = filteredUsers.filter(user =>
user.region_code === 'HU-BU-05' ||
user.region_code === 'HU-BU' ||
user.region_code === 'HU-PE' ||
user.country_code === 'HU' ||
user.scope_level === 'Global'
)
break
}
}
if (options?.search) {
const searchLower = options.search.toLowerCase()
filteredUsers = filteredUsers.filter(user =>
user.email.toLowerCase().includes(searchLower) ||
user.role.toLowerCase().includes(searchLower) ||
user.scope_level.toLowerCase().includes(searchLower) ||
(user.country_code && user.country_code.toLowerCase().includes(searchLower)) ||
(user.region_code && user.region_code.toLowerCase().includes(searchLower))
)
}
return filteredUsers
}
// Get single user by ID
async getUserById(id: number): Promise<User | null> {
await this.delay(300)
return this.mockUsers.find(user => user.id === id) || null
}
// Update user role and scope
async updateUserRole(id: number, data: UpdateUserRoleRequest): Promise<User> {
await this.delay(800) // Simulate slower update
const userIndex = this.mockUsers.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error(`User with ID ${id} not found`)
}
// Check permissions (in a real app, this would be server-side)
const authStore = useAuthStore()
const currentUserRole = authStore.getUserRole
// Superadmin can update anyone
// Admin cannot update superadmin or other admins
if (currentUserRole === 'admin') {
const targetUser = this.mockUsers[userIndex]
if (targetUser.role === 'superadmin' || (targetUser.role === 'admin' && targetUser.id !== authStore.getUserId)) {
throw new Error('Admin cannot update superadmin or other admin users')
}
}
// Update the user
const updatedUser: User = {
...this.mockUsers[userIndex],
...data,
updated_at: new Date().toISOString().split('T')[0],
}
this.mockUsers[userIndex] = updatedUser
return updatedUser
}
// Toggle user status
async toggleUserStatus(id: number): Promise<User> {
await this.delay(500)
const userIndex = this.mockUsers.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error(`User with ID ${id} not found`)
}
const currentStatus = this.mockUsers[userIndex].status
const newStatus: User['status'] = currentStatus === 'active' ? 'inactive' : 'active'
this.mockUsers[userIndex] = {
...this.mockUsers[userIndex],
status: newStatus,
updated_at: new Date().toISOString().split('T')[0],
}
return this.mockUsers[userIndex]
}
// Create new user (mock)
async createUser(email: string, role: User['role'], scope_level: User['scope_level']): Promise<User> {
await this.delay(1000)
const newUser: User = {
id: Math.max(...this.mockUsers.map(u => u.id)) + 1,
email,
role,
scope_level,
status: 'active',
created_at: new Date().toISOString().split('T')[0],
updated_at: new Date().toISOString().split('T')[0],
}
this.mockUsers.push(newUser)
return newUser
}
// Delete user (mock - just deactivate)
async deleteUser(id: number): Promise<void> {
await this.delay(700)
const userIndex = this.mockUsers.findIndex(user => user.id === id)
if (userIndex === -1) {
throw new Error(`User with ID ${id} not found`)
}
// Instead of deleting, mark as inactive
this.mockUsers[userIndex] = {
...this.mockUsers[userIndex],
status: 'inactive',
updated_at: new Date().toISOString().split('T')[0],
}
}
}
// Composable
export const useUserManagement = () => {
const state = ref<UserManagementState>({
users: [],
loading: false,
error: null,
})
const apiService = new UserManagementApiService()
// Computed
const activeUsers = computed(() => state.value.users.filter(user => user.status === 'active'))
const inactiveUsers = computed(() => state.value.users.filter(user => user.status === 'inactive'))
const superadminUsers = computed(() => state.value.users.filter(user => user.role === 'superadmin'))
const adminUsers = computed(() => state.value.users.filter(user => user.role === 'admin'))
// Actions
const fetchUsers = async (options?: {
role?: User['role']
scope_level?: User['scope_level']
status?: User['status']
search?: string
country_code?: string
region_code?: string
geographical_scope?: 'Global' | 'Hungary' | 'Pest County' | 'Budapest' | 'District V'
}) => {
state.value.loading = true
state.value.error = null
try {
const users = await apiService.getUsers(options)
state.value.users = users
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to fetch users'
console.error('Error fetching users:', error)
} finally {
state.value.loading = false
}
}
const updateUserRole = async (id: number, data: UpdateUserRoleRequest) => {
state.value.loading = true
state.value.error = null
try {
const updatedUser = await apiService.updateUserRole(id, data)
// Update local state
const userIndex = state.value.users.findIndex(user => user.id === id)
if (userIndex !== -1) {
state.value.users[userIndex] = updatedUser
}
return updatedUser
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to update user role'
console.error('Error updating user role:', error)
throw error
} finally {
state.value.loading = false
}
}
const toggleUserStatus = async (id: number) => {
state.value.loading = true
state.value.error = null
try {
const updatedUser = await apiService.toggleUserStatus(id)
// Update local state
const userIndex = state.value.users.findIndex(user => user.id === id)
if (userIndex !== -1) {
state.value.users[userIndex] = updatedUser
}
return updatedUser
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to toggle user status'
console.error('Error toggling user status:', error)
throw error
} finally {
state.value.loading = false
}
}
const createUser = async (email: string, role: User['role'], scope_level: User['scope_level']) => {
state.value.loading = true
state.value.error = null
try {
const newUser = await apiService.createUser(email, role, scope_level)
state.value.users.push(newUser)
return newUser
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to create user'
console.error('Error creating user:', error)
throw error
} finally {
state.value.loading = false
}
}
const deleteUser = async (id: number) => {
state.value.loading = true
state.value.error = null
try {
await apiService.deleteUser(id)
// Update local state (mark as inactive)
const userIndex = state.value.users.findIndex(user => user.id === id)
if (userIndex !== -1) {
state.value.users[userIndex] = {
...state.value.users[userIndex],
status: 'inactive',
updated_at: new Date().toISOString().split('T')[0],
}
}
} catch (error) {
state.value.error = error instanceof Error ? error.message : 'Failed to delete user'
console.error('Error deleting user:', error)
throw error
} finally {
state.value.loading = false
}
}
// Initialize with some data
const initialize = () => {
fetchUsers()
}
// Helper function to get geographical scopes for UI
const getGeographicalScopes = () => {
return [
{ 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' },
]
}
return {
// State
state: computed(() => state.value),
users: computed(() => state.value.users),
loading: computed(() => state.value.loading),
error: computed(() => state.value.error),
// Computed
activeUsers,
inactiveUsers,
superadminUsers,
adminUsers,
// Actions
fetchUsers,
updateUserRole,
toggleUserStatus,
createUser,
deleteUser,
initialize,
// Helper functions
getUserById: (id: number) => state.value.users.find(user => user.id === id),
filterByRole: (role: User['role']) => state.value.users.filter(user => user.role === role),
filterByScope: (scope_level: User['scope_level']) => state.value.users.filter(user => user.scope_level === scope_level),
getGeographicalScopes,
}
}
export default useUserManagement