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:
507
pages/admin/members/index.vue
Normal file
507
pages/admin/members/index.vue
Normal file
@@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<!-- Header -->
|
||||
<v-row class="mb-6">
|
||||
<v-col>
|
||||
<h1 class="text-h3 font-weight-bold mb-2">Member Management</h1>
|
||||
<p class="text-body-1 text-medium-emphasis">Manage association members and their information</p>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-account-plus"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
Add Member
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12" md="3">
|
||||
<v-card elevation="2">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ stats.total }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">Total Members</div>
|
||||
</div>
|
||||
<v-icon size="32" color="primary">mdi-account-group</v-icon>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card elevation="2">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ stats.active }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">Active Members</div>
|
||||
</div>
|
||||
<v-icon size="32" color="success">mdi-account-check</v-icon>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card elevation="2">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ stats.newThisMonth }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">New This Month</div>
|
||||
</div>
|
||||
<v-icon size="32" color="info">mdi-account-plus-outline</v-icon>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-card elevation="2">
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ stats.renewalDue }}</div>
|
||||
<div class="text-body-2 text-medium-emphasis">Renewal Due</div>
|
||||
</div>
|
||||
<v-icon size="32" color="warning">mdi-clock-alert-outline</v-icon>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Filters -->
|
||||
<v-card class="mb-6" elevation="0">
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
label="Search members"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
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="3">
|
||||
<v-select
|
||||
v-model="membershipFilter"
|
||||
label="Membership Type"
|
||||
:items="membershipOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
block
|
||||
@click="exportMembers"
|
||||
>
|
||||
<v-icon start>mdi-download</v-icon>
|
||||
Export List
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Members Table -->
|
||||
<v-card elevation="2">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filteredMembers"
|
||||
:search="searchQuery"
|
||||
:loading="loading"
|
||||
class="elevation-0"
|
||||
hover
|
||||
:items-per-page="10"
|
||||
>
|
||||
<template v-slot:item.name="{ item }">
|
||||
<div class="d-flex align-center py-2">
|
||||
<ProfileAvatar
|
||||
:member-id="item.member_id"
|
||||
:first-name="item.first_name"
|
||||
:last-name="item.last_name"
|
||||
size="40"
|
||||
class="mr-3"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ item.first_name }} {{ item.last_name }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ item.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.membership="{ item }">
|
||||
<v-chip
|
||||
:color="getMembershipColor(item.membership_type)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.membership_type }}
|
||||
</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.dues_status="{ item }">
|
||||
<v-chip
|
||||
:color="getDuesColor(item.dues_status)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item.dues_status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.join_date="{ item }">
|
||||
<span class="text-body-2">{{ formatDate(item.join_date) }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="viewMember(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="editMember(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-dots-vertical"
|
||||
size="small"
|
||||
variant="text"
|
||||
>
|
||||
<v-menu activator="parent">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="sendEmail(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-email</v-icon>
|
||||
Send Email
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="viewPaymentHistory(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-history</v-icon>
|
||||
Payment History
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="generateInvoice(item)">
|
||||
<v-list-item-title>
|
||||
<v-icon size="small" class="mr-2">mdi-file-document</v-icon>
|
||||
Generate Invoice
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider />
|
||||
<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-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- View Member Dialog -->
|
||||
<ViewMemberDialog
|
||||
v-model="showViewDialog"
|
||||
:member="selectedMember"
|
||||
@edit="handleEditMember"
|
||||
/>
|
||||
|
||||
<!-- Edit Member Dialog -->
|
||||
<EditMemberDialog
|
||||
v-model="showEditDialog"
|
||||
:member="selectedMember"
|
||||
@member-updated="handleMemberUpdated"
|
||||
/>
|
||||
|
||||
<!-- Create Member Dialog -->
|
||||
<v-dialog v-model="showCreateDialog" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title>Add New Member</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="memberForm">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="memberForm.first_name"
|
||||
label="First Name"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="memberForm.last_name"
|
||||
label="Last Name"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="memberForm.email"
|
||||
label="Email"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="memberForm.membership_type"
|
||||
label="Membership Type"
|
||||
:items="membershipOptions"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="memberForm.phone"
|
||||
label="Phone"
|
||||
variant="outlined"
|
||||
/>
|
||||
</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="saveMember">Create</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Member } from '~/utils/types';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
middleware: 'admin'
|
||||
});
|
||||
|
||||
// State
|
||||
const loading = ref(false);
|
||||
const showViewDialog = ref(false);
|
||||
const showEditDialog = ref(false);
|
||||
const showCreateDialog = ref(false);
|
||||
const selectedMember = ref<Member | null>(null);
|
||||
const searchQuery = ref('');
|
||||
const statusFilter = ref(null);
|
||||
const membershipFilter = ref(null);
|
||||
|
||||
// Stats
|
||||
const stats = ref({
|
||||
total: 156,
|
||||
active: 142,
|
||||
newThisMonth: 8,
|
||||
renewalDue: 23
|
||||
});
|
||||
|
||||
// Form data
|
||||
const memberForm = ref({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
membership_type: 'Standard',
|
||||
phone: ''
|
||||
});
|
||||
|
||||
// Options
|
||||
const statusOptions = ['active', 'inactive'];
|
||||
const membershipOptions = ['Standard', 'Premium', 'VIP', 'Lifetime'];
|
||||
|
||||
// Table configuration
|
||||
const headers = [
|
||||
{ title: 'Member', key: 'name', sortable: true },
|
||||
{ title: 'Membership', key: 'membership', sortable: true },
|
||||
{ title: 'Status', key: 'status', sortable: true },
|
||||
{ title: 'Dues', key: 'dues_status', sortable: true },
|
||||
{ title: 'Joined', key: 'join_date', sortable: true },
|
||||
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' }
|
||||
];
|
||||
|
||||
// Mock data
|
||||
const members = ref<Member[]>([
|
||||
{
|
||||
member_id: '1',
|
||||
first_name: 'John',
|
||||
last_name: 'Smith',
|
||||
email: 'john.smith@example.com',
|
||||
membership_type: 'Premium',
|
||||
status: 'active',
|
||||
dues_status: 'Paid',
|
||||
join_date: '2023-01-15',
|
||||
phone: '555-0100'
|
||||
},
|
||||
{
|
||||
member_id: '2',
|
||||
first_name: 'Sarah',
|
||||
last_name: 'Johnson',
|
||||
email: 'sarah.j@example.com',
|
||||
membership_type: 'Standard',
|
||||
status: 'active',
|
||||
dues_status: 'Due',
|
||||
join_date: '2023-03-22',
|
||||
phone: '555-0101'
|
||||
},
|
||||
{
|
||||
member_id: '3',
|
||||
first_name: 'Michael',
|
||||
last_name: 'Williams',
|
||||
email: 'michael.w@example.com',
|
||||
membership_type: 'VIP',
|
||||
status: 'active',
|
||||
dues_status: 'Paid',
|
||||
join_date: '2022-11-08',
|
||||
phone: '555-0102'
|
||||
}
|
||||
]);
|
||||
|
||||
// Computed
|
||||
const filteredMembers = computed(() => {
|
||||
let filtered = [...members.value];
|
||||
|
||||
if (statusFilter.value) {
|
||||
filtered = filtered.filter(m => m.status === statusFilter.value);
|
||||
}
|
||||
|
||||
if (membershipFilter.value) {
|
||||
filtered = filtered.filter(m => m.membership_type === membershipFilter.value);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const getMembershipColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'VIP': return 'error';
|
||||
case 'Premium': return 'warning';
|
||||
case 'Lifetime': return 'purple';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const getDuesColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Paid': return 'success';
|
||||
case 'Due': return 'warning';
|
||||
case 'Overdue': return 'error';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const viewMember = (member: Member) => {
|
||||
selectedMember.value = member;
|
||||
showViewDialog.value = true;
|
||||
};
|
||||
|
||||
const editMember = (member: Member) => {
|
||||
selectedMember.value = member;
|
||||
showEditDialog.value = true;
|
||||
};
|
||||
|
||||
const handleEditMember = (member: Member) => {
|
||||
showViewDialog.value = false;
|
||||
selectedMember.value = member;
|
||||
showEditDialog.value = true;
|
||||
};
|
||||
|
||||
const handleMemberUpdated = (member: Member) => {
|
||||
const index = members.value.findIndex(m => m.member_id === member.member_id);
|
||||
if (index > -1) {
|
||||
members.value[index] = member;
|
||||
}
|
||||
showEditDialog.value = false;
|
||||
};
|
||||
|
||||
const sendEmail = (member: Member) => {
|
||||
console.log('Send email to:', member);
|
||||
};
|
||||
|
||||
const viewPaymentHistory = (member: Member) => {
|
||||
console.log('View payment history:', member);
|
||||
};
|
||||
|
||||
const generateInvoice = (member: Member) => {
|
||||
console.log('Generate invoice:', member);
|
||||
};
|
||||
|
||||
const toggleStatus = (member: Member) => {
|
||||
member.status = member.status === 'active' ? 'inactive' : 'active';
|
||||
};
|
||||
|
||||
const exportMembers = () => {
|
||||
console.log('Export members list');
|
||||
};
|
||||
|
||||
const saveMember = () => {
|
||||
console.log('Save member:', memberForm.value);
|
||||
showCreateDialog.value = false;
|
||||
};
|
||||
|
||||
// Load data on mount
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
// Fetch members from API
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user