Files
service-finder/frontend/admin/composables/useUserManagement.ts
2026-03-23 21:43:40 +00:00

498 lines
18 KiB
TypeScript

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