admin firs step
This commit is contained in:
335
frontend/admin/composables/useHealthMonitor.ts
Normal file
335
frontend/admin/composables/useHealthMonitor.ts
Normal 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
|
||||
200
frontend/admin/composables/usePolling.ts
Normal file
200
frontend/admin/composables/usePolling.ts
Normal 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
|
||||
237
frontend/admin/composables/useRBAC.ts
Normal file
237
frontend/admin/composables/useRBAC.ts
Normal 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
|
||||
}
|
||||
}
|
||||
185
frontend/admin/composables/useServiceMap.ts
Normal file
185
frontend/admin/composables/useServiceMap.ts
Normal 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
|
||||
}
|
||||
}
|
||||
498
frontend/admin/composables/useUserManagement.ts
Normal file
498
frontend/admin/composables/useUserManagement.ts
Normal 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
|
||||
Reference in New Issue
Block a user