Complete infrastructure reorganization to role-based structure
All checks were successful
Build And Push Image / docker (push) Successful in 1m50s
All checks were successful
Build And Push Image / docker (push) Successful in 1m50s
- Created all missing admin pages (users, settings, events, members, payments) - Created board pages (governance, meetings) - Updated dashboard router to use new /admin, /board, /member structure - Added isMember alias to useAuth composable for consistency - All pages now use correct role-based layouts and middleware - Build verified successfully The platform now has a clean separation: - /admin/* - Administrator dashboard and tools - /board/* - Board member governance and meetings - /member/* - Member portal and resources Next steps: Complete remaining member pages and clean up old dashboard files
This commit is contained in:
424
pages/admin/users/index.vue
Normal file
424
pages/admin/users/index.vue
Normal file
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<!-- Header -->
|
||||
<v-row class="mb-6">
|
||||
<v-col>
|
||||
<h1 class="text-h3 font-weight-bold mb-2">User Management</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">Manage system users and permissions</p>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-account-plus"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
Add User
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Filters -->
|
||||
<v-card class="mb-6" elevation="0">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="Search users"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="roleFilter"
|
||||
label="Role"
|
||||
:items="roleOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-select
|
||||
v-model="statusFilter"
|
||||
label="Status"
|
||||
:items="statusOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
block
|
||||
@click="resetFilters"
|
||||
>
|
||||
Reset Filters
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Users Table -->
|
||||
<v-card elevation="2">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filteredUsers"
|
||||
:search="searchQuery"
|
||||
:loading="loading"
|
||||
class="elevation-0"
|
||||
hover
|
||||
>
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<v-avatar size="40" class="mr-3">
|
||||
<v-icon v-if="!item.avatar">mdi-account-circle</v-icon>
|
||||
<v-img v-else :src="item.avatar" />
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ item.name }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.role="{ item }">
|
||||
<v-chip
|
||||
:color="getRoleColor(item.role)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.role }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<v-chip
|
||||
:color="item.status === 'active' ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.lastLogin="{ item }">
|
||||
<span class="text-body-2">{{ formatDate(item.lastLogin) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editUser(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
<v-menu activator="parent">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="viewUser(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-eye</v-icon>
|
||||
View Details
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="resetPassword(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-lock-reset</v-icon>
|
||||
Reset Password
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@click="toggleStatus(item)"
|
||||
:class="item.status === 'active' ? 'text-error' : 'text-success'"
|
||||
>
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">
|
||||
{{ item.status === 'active' ? 'mdi-account-off' : 'mdi-account-check' }}
|
||||
</v-icon>
|
||||
{{ item.status === 'active' ? 'Deactivate' : 'Activate' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<v-list-item @click="deleteUser(item)" class="text-error">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-delete</v-icon>
|
||||
Delete User
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:bottom>
|
||||
<v-divider />
|
||||
<div class="d-flex align-center justify-space-between pa-4">
|
||||
<div class="text-body-2 text-medium-emphasis">
|
||||
Showing {{ filteredUsers.length }} of {{ totalUsers }} users
|
||||
</div>
|
||||
<v-pagination
|
||||
v-model="currentPage"
|
||||
:length="totalPages"
|
||||
:total-visible="5"
|
||||
density="compact"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- Create/Edit Dialog -->
|
||||
<v-dialog v-model="showCreateDialog" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
{{ editingUser ? 'Edit User' : 'Create New User' }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="userForm">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="userForm.firstName"
|
||||
label="First Name"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="userForm.lastName"
|
||||
label="Last Name"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="userForm.email"
|
||||
label="Email"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="userForm.role"
|
||||
label="Role"
|
||||
:items="roleOptions"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="userForm.status"
|
||||
label="Status"
|
||||
:items="statusOptions"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showCreateDialog = false">Cancel</v-btn>
|
||||
<v-btn color="primary" variant="flat" @click="saveUser">
|
||||
{{ editingUser ? 'Update' : 'Create' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
middleware: 'admin'
|
||||
});
|
||||
|
||||
// State
|
||||
const loading = ref(false);
|
||||
const showCreateDialog = ref(false);
|
||||
const editingUser = ref(null);
|
||||
const searchQuery = ref('');
|
||||
const roleFilter = ref(null);
|
||||
const statusFilter = ref(null);
|
||||
const currentPage = ref(1);
|
||||
|
||||
// Form data
|
||||
const userForm = ref({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
role: 'member',
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// Options
|
||||
const roleOptions = [
|
||||
{ title: 'Admin', value: 'admin' },
|
||||
{ title: 'Board', value: 'board' },
|
||||
{ title: 'Member', value: 'member' }
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ title: 'Active', value: 'active' },
|
||||
{ title: 'Inactive', value: 'inactive' }
|
||||
];
|
||||
|
||||
// Table configuration
|
||||
const headers = [
|
||||
{ title: 'User', key: 'name', sortable: true },
|
||||
{ title: 'Role', key: 'role', sortable: true },
|
||||
{ title: 'Status', key: 'status', sortable: true },
|
||||
{ title: 'Last Login', key: 'lastLogin', sortable: true },
|
||||
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' }
|
||||
];
|
||||
|
||||
// Mock data
|
||||
const users = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Smith',
|
||||
email: 'john.smith@example.com',
|
||||
role: 'admin',
|
||||
status: 'active',
|
||||
lastLogin: new Date('2024-01-15'),
|
||||
avatar: null
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Sarah Johnson',
|
||||
email: 'sarah.j@example.com',
|
||||
role: 'board',
|
||||
status: 'active',
|
||||
lastLogin: new Date('2024-01-14'),
|
||||
avatar: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Mike Wilson',
|
||||
email: 'mike.w@example.com',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
lastLogin: new Date('2024-01-13'),
|
||||
avatar: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Emma Davis',
|
||||
email: 'emma.d@example.com',
|
||||
role: 'member',
|
||||
status: 'inactive',
|
||||
lastLogin: new Date('2023-12-01'),
|
||||
avatar: null
|
||||
}
|
||||
]);
|
||||
|
||||
// Computed
|
||||
const filteredUsers = computed(() => {
|
||||
let filtered = [...users.value];
|
||||
|
||||
if (roleFilter.value) {
|
||||
filtered = filtered.filter(u => u.role === roleFilter.value);
|
||||
}
|
||||
|
||||
if (statusFilter.value) {
|
||||
filtered = filtered.filter(u => u.status === statusFilter.value);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const totalUsers = computed(() => users.value.length);
|
||||
const totalPages = computed(() => Math.ceil(filteredUsers.value.length / 10));
|
||||
|
||||
// Methods
|
||||
const getRoleColor = (role: string) => {
|
||||
switch (role) {
|
||||
case 'admin': return 'error';
|
||||
case 'board': return 'warning';
|
||||
case 'member': return 'info';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
if (!date) return 'Never';
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const resetFilters = () => {
|
||||
searchQuery.value = '';
|
||||
roleFilter.value = null;
|
||||
statusFilter.value = null;
|
||||
};
|
||||
|
||||
const editUser = (user: any) => {
|
||||
editingUser.value = user;
|
||||
userForm.value = {
|
||||
firstName: user.name.split(' ')[0],
|
||||
lastName: user.name.split(' ')[1] || '',
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
status: user.status
|
||||
};
|
||||
showCreateDialog.value = true;
|
||||
};
|
||||
|
||||
const viewUser = (user: any) => {
|
||||
console.log('View user:', user);
|
||||
};
|
||||
|
||||
const resetPassword = (user: any) => {
|
||||
console.log('Reset password for:', user);
|
||||
};
|
||||
|
||||
const toggleStatus = (user: any) => {
|
||||
user.status = user.status === 'active' ? 'inactive' : 'active';
|
||||
};
|
||||
|
||||
const deleteUser = (user: any) => {
|
||||
console.log('Delete user:', user);
|
||||
};
|
||||
|
||||
const saveUser = () => {
|
||||
console.log('Save user:', userForm.value);
|
||||
showCreateDialog.value = false;
|
||||
editingUser.value = null;
|
||||
};
|
||||
|
||||
// Load data on mount
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
// Fetch users from API
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user