admin firs step
This commit is contained in:
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