498 lines
18 KiB
TypeScript
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 |