Complete infrastructure reorganization to role-based structure
Build And Push Image / docker (push) Successful in 1m50s Details

- 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:
Matt 2025-08-30 22:44:04 +02:00
parent 7c49b9db66
commit 9fa9db9b8a
9 changed files with 3177 additions and 3 deletions

View File

@ -41,6 +41,9 @@ export const useAuth = () => {
// Fallback to legacy tier system
return user.value?.tier === 'user';
});
// Alias for consistency with new naming convention
const isMember = isUser;
const isBoard = computed(() => {
// Check new realm roles first
@ -300,6 +303,7 @@ export const useAuth = () => {
// Tier-based properties
userTier,
isUser,
isMember, // Alias for isUser, better naming convention
isBoard,
isAdmin,
firstName,

View File

@ -0,0 +1,552 @@
<template>
<v-container fluid>
<!-- Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold mb-2">Event Management</h1>
<p class="text-body-1 text-medium-emphasis">Create and manage association events</p>
</v-col>
<v-col cols="auto">
<v-btn
color="primary"
variant="flat"
prepend-icon="mdi-calendar-plus"
@click="showCreateDialog = true"
>
Create Event
</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.upcoming }}</div>
<div class="text-body-2 text-medium-emphasis">Upcoming Events</div>
</div>
<v-icon size="32" color="primary">mdi-calendar-clock</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.totalRegistrations }}</div>
<div class="text-body-2 text-medium-emphasis">Total Registrations</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.revenue }}</div>
<div class="text-body-2 text-medium-emphasis">Total Revenue</div>
</div>
<v-icon size="32" color="warning">mdi-cash</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.avgAttendance }}%</div>
<div class="text-body-2 text-medium-emphasis">Avg Attendance</div>
</div>
<v-icon size="32" color="info">mdi-chart-line</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 events"
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="typeFilter"
label="Event Type"
:items="typeOptions"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="dateRange"
label="Date Range"
:items="dateRangeOptions"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Events List -->
<v-card elevation="2">
<v-data-table
:headers="headers"
:items="filteredEvents"
:search="searchQuery"
:loading="loading"
class="elevation-0"
hover
>
<template v-slot:item.title="{ item }">
<div class="py-2">
<div class="font-weight-medium">{{ item.title }}</div>
<div class="text-caption text-medium-emphasis">{{ item.type }}</div>
</div>
</template>
<template v-slot:item.date="{ item }">
<div>
<div class="text-body-2">{{ formatDate(item.date) }}</div>
<div class="text-caption text-medium-emphasis">{{ item.time }}</div>
</div>
</template>
<template v-slot:item.registrations="{ item }">
<div class="d-flex align-center">
<v-progress-linear
:model-value="(item.registrations / item.capacity) * 100"
:color="getCapacityColor(item.registrations, item.capacity)"
height="6"
rounded
class="mr-2"
style="min-width: 60px"
/>
<span class="text-body-2">
{{ item.registrations }}/{{ item.capacity }}
</span>
</div>
</template>
<template v-slot:item.status="{ item }">
<v-chip
:color="getStatusColor(item.status)"
size="small"
variant="tonal"
>
{{ item.status }}
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
icon="mdi-eye"
size="small"
variant="text"
@click="viewEvent(item)"
/>
<v-btn
icon="mdi-pencil"
size="small"
variant="text"
@click="editEvent(item)"
/>
<v-btn
icon="mdi-dots-vertical"
size="small"
variant="text"
>
<v-menu activator="parent">
<v-list density="compact">
<v-list-item @click="duplicateEvent(item)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-content-copy</v-icon>
Duplicate
</v-list-item-title>
</v-list-item>
<v-list-item @click="viewAttendees(item)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-account-group</v-icon>
View Attendees
</v-list-item-title>
</v-list-item>
<v-list-item @click="exportEvent(item)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-download</v-icon>
Export Data
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item
@click="cancelEvent(item)"
class="text-error"
:disabled="item.status === 'cancelled'"
>
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-cancel</v-icon>
Cancel Event
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</template>
</v-data-table>
</v-card>
<!-- Create/Edit Event Dialog -->
<v-dialog v-model="showCreateDialog" max-width="800">
<v-card>
<v-card-title>
{{ editingEvent ? 'Edit Event' : 'Create New Event' }}
</v-card-title>
<v-card-text>
<v-form ref="eventForm">
<v-row>
<v-col cols="12">
<v-text-field
v-model="eventForm.title"
label="Event Title"
variant="outlined"
required
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="eventForm.description"
label="Description"
variant="outlined"
rows="3"
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="eventForm.type"
label="Event Type"
:items="typeOptions"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="eventForm.location"
label="Location"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="eventForm.date"
label="Date"
type="date"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="eventForm.time"
label="Time"
type="time"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="eventForm.duration"
label="Duration (hours)"
type="number"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="eventForm.capacity"
label="Capacity"
type="number"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="eventForm.price"
label="Price"
prefix="$"
type="number"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="eventForm.registrationType"
label="Registration"
:items="['Open', 'Members Only', 'Invite Only']"
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="saveEvent">
{{ editingEvent ? '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 editingEvent = ref(null);
const searchQuery = ref('');
const statusFilter = ref(null);
const typeFilter = ref(null);
const dateRange = ref(null);
// Stats
const stats = ref({
upcoming: 8,
totalRegistrations: 342,
revenue: 15420,
avgAttendance: 78
});
// Form data
const eventForm = ref({
title: '',
description: '',
type: '',
location: '',
date: '',
time: '',
duration: 2,
capacity: 50,
price: 0,
registrationType: 'Open'
});
// Options
const statusOptions = [
'Upcoming',
'Ongoing',
'Completed',
'Cancelled'
];
const typeOptions = [
'Conference',
'Workshop',
'Networking',
'Social',
'Fundraiser',
'Meeting'
];
const dateRangeOptions = [
'This Week',
'This Month',
'Next Month',
'This Quarter',
'This Year'
];
// Table configuration
const headers = [
{ title: 'Event', key: 'title', sortable: true },
{ title: 'Date & Time', key: 'date', sortable: true },
{ title: 'Location', key: 'location', sortable: true },
{ title: 'Registrations', key: 'registrations', sortable: true },
{ title: 'Status', key: 'status', sortable: true },
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' }
];
// Mock data
const events = ref([
{
id: 1,
title: 'Annual Gala Dinner',
type: 'Fundraiser',
date: new Date('2024-02-15'),
time: '19:00',
location: 'Grand Ballroom',
registrations: 145,
capacity: 200,
status: 'Upcoming',
price: 250
},
{
id: 2,
title: 'Business Networking Event',
type: 'Networking',
date: new Date('2024-01-22'),
time: '18:00',
location: 'Conference Center',
registrations: 48,
capacity: 50,
status: 'Upcoming',
price: 0
},
{
id: 3,
title: 'Digital Marketing Workshop',
type: 'Workshop',
date: new Date('2024-01-10'),
time: '14:00',
location: 'Training Room A',
registrations: 22,
capacity: 30,
status: 'Completed',
price: 75
},
{
id: 4,
title: 'Board Meeting',
type: 'Meeting',
date: new Date('2024-01-05'),
time: '10:00',
location: 'Board Room',
registrations: 12,
capacity: 15,
status: 'Completed',
price: 0
}
]);
// Computed
const filteredEvents = computed(() => {
let filtered = [...events.value];
if (statusFilter.value) {
filtered = filtered.filter(e => e.status === statusFilter.value);
}
if (typeFilter.value) {
filtered = filtered.filter(e => e.type === typeFilter.value);
}
return filtered;
});
// Methods
const getStatusColor = (status: string) => {
switch (status) {
case 'Upcoming': return 'info';
case 'Ongoing': return 'success';
case 'Completed': return 'default';
case 'Cancelled': return 'error';
default: return 'default';
}
};
const getCapacityColor = (registrations: number, capacity: number) => {
const percentage = (registrations / capacity) * 100;
if (percentage >= 90) return 'error';
if (percentage >= 70) return 'warning';
return 'success';
};
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const viewEvent = (event: any) => {
console.log('View event:', event);
};
const editEvent = (event: any) => {
editingEvent.value = event;
eventForm.value = {
title: event.title,
description: '',
type: event.type,
location: event.location,
date: event.date.toISOString().split('T')[0],
time: event.time,
duration: 2,
capacity: event.capacity,
price: event.price,
registrationType: 'Open'
};
showCreateDialog.value = true;
};
const duplicateEvent = (event: any) => {
console.log('Duplicate event:', event);
};
const viewAttendees = (event: any) => {
console.log('View attendees:', event);
};
const exportEvent = (event: any) => {
console.log('Export event:', event);
};
const cancelEvent = (event: any) => {
console.log('Cancel event:', event);
event.status = 'Cancelled';
};
const saveEvent = () => {
console.log('Save event:', eventForm.value);
showCreateDialog.value = false;
editingEvent.value = null;
};
</script>

View 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>

View File

@ -0,0 +1,524 @@
<template>
<v-container fluid>
<!-- Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold mb-2">Payment Management</h1>
<p class="text-body-1 text-medium-emphasis">Track and manage all payments and transactions</p>
</v-col>
<v-col cols="auto">
<v-btn
color="primary"
variant="flat"
prepend-icon="mdi-cash-plus"
@click="showRecordPaymentDialog = true"
>
Record Payment
</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.totalRevenue.toLocaleString() }}</div>
<div class="text-body-2 text-medium-emphasis">Total Revenue</div>
</div>
<v-icon size="32" color="success">mdi-cash</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.pending.toLocaleString() }}</div>
<div class="text-body-2 text-medium-emphasis">Pending</div>
</div>
<v-icon size="32" color="warning">mdi-clock-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.overdue.toLocaleString() }}</div>
<div class="text-body-2 text-medium-emphasis">Overdue</div>
</div>
<v-icon size="32" color="error">mdi-alert-circle-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.transactions }}</div>
<div class="text-body-2 text-medium-emphasis">Transactions</div>
</div>
<v-icon size="32" color="info">mdi-swap-horizontal</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 payments"
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
<v-col cols="12" md="2">
<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-select
v-model="typeFilter"
label="Type"
:items="typeOptions"
variant="outlined"
density="compact"
clearable
hide-details
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="dateFrom"
label="From Date"
type="date"
variant="outlined"
density="compact"
hide-details
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="dateTo"
label="To Date"
type="date"
variant="outlined"
density="compact"
hide-details
/>
</v-col>
<v-col cols="12" md="1">
<v-btn
variant="outlined"
color="primary"
block
@click="exportPayments"
>
Export
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Payments Table -->
<v-card elevation="2">
<v-data-table
:headers="headers"
:items="filteredPayments"
:search="searchQuery"
:loading="loading"
class="elevation-0"
hover
:items-per-page="10"
>
<template v-slot:item.transaction_id="{ item }">
<code class="text-caption">{{ item.transaction_id }}</code>
</template>
<template v-slot:item.member="{ item }">
<div class="py-2">
<div class="font-weight-medium">{{ item.member_name }}</div>
<div class="text-caption text-medium-emphasis">{{ item.member_email }}</div>
</div>
</template>
<template v-slot:item.amount="{ item }">
<span class="font-weight-medium">${{ item.amount.toFixed(2) }}</span>
</template>
<template v-slot:item.type="{ item }">
<v-chip
:color="getTypeColor(item.type)"
size="small"
variant="tonal"
>
{{ item.type }}
</v-chip>
</template>
<template v-slot:item.status="{ item }">
<v-chip
:color="getStatusColor(item.status)"
size="small"
variant="tonal"
>
{{ item.status }}
</v-chip>
</template>
<template v-slot:item.date="{ item }">
<span class="text-body-2">{{ formatDate(item.date) }}</span>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
icon="mdi-eye"
size="small"
variant="text"
@click="viewPayment(item)"
/>
<v-btn
icon="mdi-dots-vertical"
size="small"
variant="text"
>
<v-menu activator="parent">
<v-list density="compact">
<v-list-item @click="viewReceipt(item)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-receipt</v-icon>
View Receipt
</v-list-item-title>
</v-list-item>
<v-list-item @click="sendReceipt(item)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-email</v-icon>
Email Receipt
</v-list-item-title>
</v-list-item>
<v-list-item
@click="refundPayment(item)"
:disabled="item.status !== 'Completed'"
>
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-cash-refund</v-icon>
Issue Refund
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item
@click="markAsPaid(item)"
:disabled="item.status === 'Completed'"
class="text-success"
>
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-check</v-icon>
Mark as Paid
</v-list-item-title>
</v-list-item>
<v-list-item
@click="voidPayment(item)"
class="text-error"
:disabled="item.status === 'Voided'"
>
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-cancel</v-icon>
Void Payment
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</template>
</v-data-table>
</v-card>
<!-- Record Payment Dialog -->
<v-dialog v-model="showRecordPaymentDialog" max-width="600">
<v-card>
<v-card-title>Record Payment</v-card-title>
<v-card-text>
<v-form ref="paymentForm">
<v-row>
<v-col cols="12">
<v-autocomplete
v-model="paymentForm.member_id"
label="Member"
:items="membersList"
item-title="name"
item-value="id"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="paymentForm.type"
label="Payment Type"
:items="typeOptions"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="paymentForm.amount"
label="Amount"
prefix="$"
type="number"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="paymentForm.method"
label="Payment Method"
:items="['Credit Card', 'Check', 'Cash', 'Bank Transfer']"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="paymentForm.reference"
label="Reference Number"
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="paymentForm.notes"
label="Notes"
variant="outlined"
rows="2"
/>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="showRecordPaymentDialog = false">Cancel</v-btn>
<v-btn color="primary" variant="flat" @click="savePayment">Record</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 showRecordPaymentDialog = ref(false);
const searchQuery = ref('');
const statusFilter = ref(null);
const typeFilter = ref(null);
const dateFrom = ref('');
const dateTo = ref('');
// Stats
const stats = ref({
totalRevenue: 45820,
pending: 3250,
overdue: 1800,
transactions: 342
});
// Form data
const paymentForm = ref({
member_id: '',
type: '',
amount: 0,
method: '',
reference: '',
notes: ''
});
// Options
const statusOptions = ['Completed', 'Pending', 'Failed', 'Refunded', 'Voided'];
const typeOptions = ['Membership', 'Event', 'Donation', 'Other'];
// Mock members list
const membersList = [
{ id: '1', name: 'John Smith' },
{ id: '2', name: 'Sarah Johnson' },
{ id: '3', name: 'Michael Williams' }
];
// Table configuration
const headers = [
{ title: 'Transaction ID', key: 'transaction_id', sortable: true },
{ title: 'Member', key: 'member', sortable: true },
{ title: 'Amount', key: 'amount', sortable: true },
{ title: 'Type', key: 'type', sortable: true },
{ title: 'Status', key: 'status', sortable: true },
{ title: 'Date', key: 'date', sortable: true },
{ title: 'Actions', key: 'actions', sortable: false, align: 'end' }
];
// Mock data
const payments = ref([
{
id: 1,
transaction_id: 'TXN-2024-001',
member_name: 'John Smith',
member_email: 'john.smith@example.com',
amount: 500,
type: 'Membership',
status: 'Completed',
date: new Date('2024-01-15'),
method: 'Credit Card'
},
{
id: 2,
transaction_id: 'TXN-2024-002',
member_name: 'Sarah Johnson',
member_email: 'sarah.j@example.com',
amount: 250,
type: 'Event',
status: 'Pending',
date: new Date('2024-01-14'),
method: 'Bank Transfer'
},
{
id: 3,
transaction_id: 'TXN-2024-003',
member_name: 'Michael Williams',
member_email: 'michael.w@example.com',
amount: 1000,
type: 'Donation',
status: 'Completed',
date: new Date('2024-01-13'),
method: 'Check'
},
{
id: 4,
transaction_id: 'TXN-2024-004',
member_name: 'Emma Davis',
member_email: 'emma.d@example.com',
amount: 75,
type: 'Event',
status: 'Failed',
date: new Date('2024-01-12'),
method: 'Credit Card'
}
]);
// Computed
const filteredPayments = computed(() => {
let filtered = [...payments.value];
if (statusFilter.value) {
filtered = filtered.filter(p => p.status === statusFilter.value);
}
if (typeFilter.value) {
filtered = filtered.filter(p => p.type === typeFilter.value);
}
if (dateFrom.value) {
const from = new Date(dateFrom.value);
filtered = filtered.filter(p => new Date(p.date) >= from);
}
if (dateTo.value) {
const to = new Date(dateTo.value);
filtered = filtered.filter(p => new Date(p.date) <= to);
}
return filtered;
});
// Methods
const getStatusColor = (status: string) => {
switch (status) {
case 'Completed': return 'success';
case 'Pending': return 'warning';
case 'Failed': return 'error';
case 'Refunded': return 'info';
case 'Voided': return 'default';
default: return 'default';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'Membership': return 'primary';
case 'Event': return 'info';
case 'Donation': return 'success';
default: return 'default';
}
};
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const viewPayment = (payment: any) => {
console.log('View payment:', payment);
};
const viewReceipt = (payment: any) => {
console.log('View receipt:', payment);
};
const sendReceipt = (payment: any) => {
console.log('Send receipt:', payment);
};
const refundPayment = (payment: any) => {
console.log('Refund payment:', payment);
};
const markAsPaid = (payment: any) => {
payment.status = 'Completed';
};
const voidPayment = (payment: any) => {
payment.status = 'Voided';
};
const exportPayments = () => {
console.log('Export payments');
};
const savePayment = () => {
console.log('Save payment:', paymentForm.value);
showRecordPaymentDialog.value = false;
};
</script>

View File

@ -0,0 +1,513 @@
<template>
<v-container fluid>
<!-- Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold mb-2">System Settings</h1>
<p class="text-body-1 text-medium-emphasis">Configure system preferences and options</p>
</v-col>
</v-row>
<!-- Settings Tabs -->
<v-card elevation="2">
<v-tabs v-model="activeTab" color="primary">
<v-tab value="general">
<v-icon start>mdi-cog</v-icon>
General
</v-tab>
<v-tab value="security">
<v-icon start>mdi-shield-lock</v-icon>
Security
</v-tab>
<v-tab value="email">
<v-icon start>mdi-email</v-icon>
Email
</v-tab>
<v-tab value="payments">
<v-icon start>mdi-credit-card</v-icon>
Payments
</v-tab>
<v-tab value="integrations">
<v-icon start>mdi-api</v-icon>
Integrations
</v-tab>
</v-tabs>
<v-window v-model="activeTab">
<!-- General Settings -->
<v-window-item value="general">
<v-card-text>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">Organization Information</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.general.orgName"
label="Organization Name"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.general.orgEmail"
label="Contact Email"
variant="outlined"
type="email"
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="settings.general.orgDescription"
label="Description"
variant="outlined"
rows="3"
/>
</v-col>
<v-col cols="12">
<v-divider class="my-4" />
</v-col>
<v-col cols="12">
<h3 class="text-h6 mb-4">Regional Settings</h3>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="settings.general.timezone"
label="Timezone"
:items="timezones"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="settings.general.dateFormat"
label="Date Format"
:items="dateFormats"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="4">
<v-select
v-model="settings.general.currency"
label="Currency"
:items="currencies"
variant="outlined"
/>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- Security Settings -->
<v-window-item value="security">
<v-card-text>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">Authentication</h3>
</v-col>
<v-col cols="12">
<v-switch
v-model="settings.security.twoFactor"
label="Require Two-Factor Authentication"
color="primary"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="settings.security.sso"
label="Enable Single Sign-On (SSO)"
color="primary"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.security.sessionTimeout"
label="Session Timeout (minutes)"
variant="outlined"
type="number"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.security.maxLoginAttempts"
label="Max Login Attempts"
variant="outlined"
type="number"
/>
</v-col>
<v-col cols="12">
<v-divider class="my-4" />
</v-col>
<v-col cols="12">
<h3 class="text-h6 mb-4">Password Policy</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.security.minPasswordLength"
label="Minimum Password Length"
variant="outlined"
type="number"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.security.passwordExpiry"
label="Password Expiry (days)"
variant="outlined"
type="number"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="settings.security.requireSpecialChar"
label="Require Special Characters"
color="primary"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="settings.security.requireNumbers"
label="Require Numbers"
color="primary"
/>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- Email Settings -->
<v-window-item value="email">
<v-card-text>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">SMTP Configuration</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpHost"
label="SMTP Host"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpPort"
label="SMTP Port"
variant="outlined"
type="number"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpUsername"
label="SMTP Username"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.smtpPassword"
label="SMTP Password"
variant="outlined"
type="password"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="settings.email.useTLS"
label="Use TLS/SSL"
color="primary"
/>
</v-col>
<v-col cols="12">
<v-divider class="my-4" />
</v-col>
<v-col cols="12">
<h3 class="text-h6 mb-4">Email Templates</h3>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.fromName"
label="From Name"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.email.fromEmail"
label="From Email"
variant="outlined"
type="email"
/>
</v-col>
<v-col cols="12">
<v-btn variant="outlined" color="primary">
<v-icon start>mdi-email-edit</v-icon>
Manage Email Templates
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- Payment Settings -->
<v-window-item value="payments">
<v-card-text>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">Payment Gateway</h3>
</v-col>
<v-col cols="12">
<v-radio-group v-model="settings.payments.gateway" row>
<v-radio label="Stripe" value="stripe" />
<v-radio label="PayPal" value="paypal" />
<v-radio label="Square" value="square" />
</v-radio-group>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.payments.publicKey"
label="Public Key"
variant="outlined"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="settings.payments.secretKey"
label="Secret Key"
variant="outlined"
type="password"
/>
</v-col>
<v-col cols="12">
<v-divider class="my-4" />
</v-col>
<v-col cols="12">
<h3 class="text-h6 mb-4">Membership Fees</h3>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="settings.payments.membershipFee"
label="Annual Membership Fee"
variant="outlined"
prefix="$"
type="number"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="settings.payments.boardFee"
label="Board Member Fee"
variant="outlined"
prefix="$"
type="number"
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="settings.payments.lateFee"
label="Late Payment Fee"
variant="outlined"
prefix="$"
type="number"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="settings.payments.autoRenew"
label="Enable Auto-Renewal"
color="primary"
/>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- Integrations -->
<v-window-item value="integrations">
<v-card-text>
<v-row>
<v-col cols="12">
<h3 class="text-h6 mb-4">Third-Party Integrations</h3>
</v-col>
<v-col cols="12">
<v-list>
<v-list-item
v-for="integration in integrations"
:key="integration.id"
class="px-0"
>
<v-card variant="outlined" class="w-100">
<v-card-text>
<v-row align="center">
<v-col cols="auto">
<v-icon :icon="integration.icon" size="32" />
</v-col>
<v-col>
<div class="font-weight-medium">{{ integration.name }}</div>
<div class="text-caption text-medium-emphasis">
{{ integration.description }}
</div>
</v-col>
<v-col cols="auto">
<v-switch
v-model="integration.enabled"
color="primary"
hide-details
/>
</v-col>
<v-col cols="auto">
<v-btn
variant="outlined"
size="small"
:disabled="!integration.enabled"
>
Configure
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-list-item>
</v-list>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
</v-window>
<v-divider />
<!-- Actions -->
<v-card-actions class="pa-4">
<v-spacer />
<v-btn variant="outlined" @click="resetSettings">
Reset to Defaults
</v-btn>
<v-btn color="primary" variant="flat" @click="saveSettings">
Save Changes
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'admin',
middleware: 'admin'
});
// State
const activeTab = ref('general');
// Settings data
const settings = ref({
general: {
orgName: 'MonacoUSA',
orgEmail: 'info@monacousa.org',
orgDescription: 'Monaco USA Association - Connecting Monaco and USA',
timezone: 'America/New_York',
dateFormat: 'MM/DD/YYYY',
currency: 'USD'
},
security: {
twoFactor: false,
sso: true,
sessionTimeout: 30,
maxLoginAttempts: 5,
minPasswordLength: 8,
passwordExpiry: 90,
requireSpecialChar: true,
requireNumbers: true
},
email: {
smtpHost: 'smtp.gmail.com',
smtpPort: 587,
smtpUsername: '',
smtpPassword: '',
useTLS: true,
fromName: 'MonacoUSA',
fromEmail: 'noreply@monacousa.org'
},
payments: {
gateway: 'stripe',
publicKey: '',
secretKey: '',
membershipFee: 500,
boardFee: 1000,
lateFee: 50,
autoRenew: true
}
});
// Options
const timezones = [
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'Europe/Monaco'
];
const dateFormats = [
'MM/DD/YYYY',
'DD/MM/YYYY',
'YYYY-MM-DD'
];
const currencies = [
'USD',
'EUR',
'GBP'
];
const integrations = ref([
{
id: 1,
name: 'Google Calendar',
description: 'Sync events with Google Calendar',
icon: 'mdi-google',
enabled: true
},
{
id: 2,
name: 'Mailchimp',
description: 'Email marketing and newsletters',
icon: 'mdi-email-newsletter',
enabled: false
},
{
id: 3,
name: 'Slack',
description: 'Team communication and notifications',
icon: 'mdi-slack',
enabled: false
},
{
id: 4,
name: 'QuickBooks',
description: 'Accounting and financial management',
icon: 'mdi-calculator',
enabled: true
},
{
id: 5,
name: 'Zoom',
description: 'Virtual meetings and webinars',
icon: 'mdi-video',
enabled: true
}
]);
// Methods
const saveSettings = () => {
console.log('Saving settings:', settings.value);
// Save to API
};
const resetSettings = () => {
console.log('Resetting to defaults');
// Reset to default values
};
</script>

424
pages/admin/users/index.vue Normal file
View 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>

View File

@ -0,0 +1,350 @@
<template>
<v-container fluid>
<!-- Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold mb-2">Governance Documents</h1>
<p class="text-body-1 text-medium-emphasis">Access bylaws, policies, and governance materials</p>
</v-col>
<v-col cols="auto">
<v-btn
color="primary"
variant="flat"
prepend-icon="mdi-file-upload"
@click="showUploadDialog = true"
>
Upload Document
</v-btn>
</v-col>
</v-row>
<!-- Document Categories -->
<v-row class="mb-6">
<v-col cols="12">
<v-chip-group
v-model="selectedCategory"
selected-class="bg-primary"
mandatory
>
<v-chip
v-for="category in categories"
:key="category.value"
:value="category.value"
variant="outlined"
>
<v-icon start>{{ category.icon }}</v-icon>
{{ category.title }}
<v-badge
:content="category.count"
color="primary"
inline
class="ml-2"
/>
</v-chip>
</v-chip-group>
</v-col>
</v-row>
<!-- Documents List -->
<v-row>
<v-col
v-for="document in filteredDocuments"
:key="document.id"
cols="12"
md="6"
lg="4"
>
<v-card elevation="2" hover>
<v-card-text>
<div class="d-flex align-start">
<v-icon
:icon="getDocumentIcon(document.type)"
size="40"
:color="getDocumentColor(document.type)"
class="mr-3"
/>
<div class="flex-grow-1">
<h3 class="text-body-1 font-weight-medium mb-1">
{{ document.title }}
</h3>
<p class="text-caption text-medium-emphasis mb-2">
{{ document.description }}
</p>
<div class="d-flex align-center text-caption">
<v-icon size="x-small" class="mr-1">mdi-calendar</v-icon>
<span class="text-medium-emphasis">
Updated {{ formatDate(document.updatedAt) }}
</span>
</div>
<div class="d-flex align-center text-caption mt-1">
<v-icon size="x-small" class="mr-1">mdi-file-outline</v-icon>
<span class="text-medium-emphasis">
{{ document.fileSize }}
</span>
</div>
</div>
</div>
</v-card-text>
<v-divider />
<v-card-actions>
<v-btn
variant="text"
color="primary"
size="small"
@click="viewDocument(document)"
>
<v-icon start>mdi-eye</v-icon>
View
</v-btn>
<v-btn
variant="text"
color="primary"
size="small"
@click="downloadDocument(document)"
>
<v-icon start>mdi-download</v-icon>
Download
</v-btn>
<v-spacer />
<v-btn
icon="mdi-dots-vertical"
size="small"
variant="text"
>
<v-menu activator="parent">
<v-list density="compact">
<v-list-item @click="shareDocument(document)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-share-variant</v-icon>
Share
</v-list-item-title>
</v-list-item>
<v-list-item @click="editDocument(document)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-pencil</v-icon>
Edit Details
</v-list-item-title>
</v-list-item>
<v-list-item @click="archiveDocument(document)">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-archive</v-icon>
Archive
</v-list-item-title>
</v-list-item>
<v-divider />
<v-list-item @click="deleteDocument(document)" class="text-error">
<v-list-item-title>
<v-icon size="small" class="mr-2">mdi-delete</v-icon>
Delete
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<!-- Upload Dialog -->
<v-dialog v-model="showUploadDialog" max-width="600">
<v-card>
<v-card-title>Upload Document</v-card-title>
<v-card-text>
<v-form ref="uploadForm">
<v-row>
<v-col cols="12">
<v-file-input
v-model="uploadForm.file"
label="Select Document"
accept=".pdf,.doc,.docx"
variant="outlined"
prepend-icon="mdi-file-document"
required
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="uploadForm.title"
label="Document Title"
variant="outlined"
required
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="uploadForm.description"
label="Description"
variant="outlined"
rows="2"
/>
</v-col>
<v-col cols="12">
<v-select
v-model="uploadForm.category"
label="Category"
:items="categories"
item-title="title"
item-value="value"
variant="outlined"
required
/>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="showUploadDialog = false">Cancel</v-btn>
<v-btn color="primary" variant="flat" @click="uploadDocument">Upload</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'board',
middleware: 'board'
});
// State
const showUploadDialog = ref(false);
const selectedCategory = ref('all');
// Form data
const uploadForm = ref({
file: null,
title: '',
description: '',
category: ''
});
// Categories
const categories = [
{ title: 'All Documents', value: 'all', icon: 'mdi-file-multiple', count: 12 },
{ title: 'Bylaws', value: 'bylaws', icon: 'mdi-gavel', count: 2 },
{ title: 'Policies', value: 'policies', icon: 'mdi-shield-check', count: 4 },
{ title: 'Minutes', value: 'minutes', icon: 'mdi-clock-outline', count: 3 },
{ title: 'Reports', value: 'reports', icon: 'mdi-chart-box', count: 3 }
];
// Mock documents
const documents = ref([
{
id: 1,
title: 'Association Bylaws 2024',
description: 'Updated bylaws governing the association operations',
type: 'bylaws',
fileSize: '2.4 MB',
updatedAt: new Date('2024-01-01')
},
{
id: 2,
title: 'Code of Conduct Policy',
description: 'Member code of conduct and ethics guidelines',
type: 'policies',
fileSize: '548 KB',
updatedAt: new Date('2023-12-15')
},
{
id: 3,
title: 'Board Meeting Minutes - January 2024',
description: 'Minutes from the January board meeting',
type: 'minutes',
fileSize: '128 KB',
updatedAt: new Date('2024-01-10')
},
{
id: 4,
title: 'Annual Financial Report 2023',
description: 'Comprehensive financial report for fiscal year 2023',
type: 'reports',
fileSize: '4.2 MB',
updatedAt: new Date('2024-01-05')
},
{
id: 5,
title: 'Conflict of Interest Policy',
description: 'Policy for managing conflicts of interest',
type: 'policies',
fileSize: '315 KB',
updatedAt: new Date('2023-11-20')
},
{
id: 6,
title: 'Strategic Plan 2024-2026',
description: 'Three-year strategic planning document',
type: 'reports',
fileSize: '1.8 MB',
updatedAt: new Date('2023-12-01')
}
]);
// Computed
const filteredDocuments = computed(() => {
if (selectedCategory.value === 'all') {
return documents.value;
}
return documents.value.filter(d => d.type === selectedCategory.value);
});
// Methods
const getDocumentIcon = (type: string) => {
switch (type) {
case 'bylaws': return 'mdi-gavel';
case 'policies': return 'mdi-shield-check';
case 'minutes': return 'mdi-clock-outline';
case 'reports': return 'mdi-chart-box';
default: return 'mdi-file-document';
}
};
const getDocumentColor = (type: string) => {
switch (type) {
case 'bylaws': return 'error';
case 'policies': return 'warning';
case 'minutes': return 'info';
case 'reports': return 'success';
default: return 'primary';
}
};
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const viewDocument = (document: any) => {
console.log('View document:', document);
};
const downloadDocument = (document: any) => {
console.log('Download document:', document);
};
const shareDocument = (document: any) => {
console.log('Share document:', document);
};
const editDocument = (document: any) => {
console.log('Edit document:', document);
};
const archiveDocument = (document: any) => {
console.log('Archive document:', document);
};
const deleteDocument = (document: any) => {
console.log('Delete document:', document);
};
const uploadDocument = () => {
console.log('Upload document:', uploadForm.value);
showUploadDialog.value = false;
};
</script>

View File

@ -0,0 +1,293 @@
<template>
<v-container fluid>
<!-- Header -->
<v-row class="mb-6">
<v-col>
<h1 class="text-h3 font-weight-bold mb-2">Board Meetings</h1>
<p class="text-body-1 text-medium-emphasis">Schedule and manage board meetings</p>
</v-col>
<v-col cols="auto">
<v-btn
color="primary"
variant="flat"
prepend-icon="mdi-calendar-plus"
@click="showScheduleDialog = true"
>
Schedule Meeting
</v-btn>
</v-col>
</v-row>
<!-- Meeting Tabs -->
<v-tabs v-model="activeTab" color="primary" class="mb-6">
<v-tab value="upcoming">Upcoming</v-tab>
<v-tab value="past">Past Meetings</v-tab>
<v-tab value="calendar">Calendar View</v-tab>
</v-tabs>
<v-window v-model="activeTab">
<!-- Upcoming Meetings -->
<v-window-item value="upcoming">
<v-row>
<v-col
v-for="meeting in upcomingMeetings"
:key="meeting.id"
cols="12"
>
<v-card elevation="2" class="mb-3">
<v-card-text>
<v-row align="center">
<v-col cols="auto">
<v-avatar color="primary" size="56">
<v-icon>mdi-calendar</v-icon>
</v-avatar>
</v-col>
<v-col>
<h3 class="text-h6 mb-1">{{ meeting.title }}</h3>
<div class="text-body-2 text-medium-emphasis mb-2">
<v-icon size="small" class="mr-1">mdi-calendar</v-icon>
{{ formatDate(meeting.date) }}
<v-icon size="small" class="ml-3 mr-1">mdi-clock</v-icon>
{{ meeting.time }}
<v-icon size="small" class="ml-3 mr-1">mdi-map-marker</v-icon>
{{ meeting.location }}
</div>
<div class="text-body-2">
<v-chip size="small" variant="tonal" class="mr-2">
<v-icon start size="small">mdi-account-group</v-icon>
{{ meeting.attendees }} Confirmed
</v-chip>
<v-chip size="small" variant="tonal" color="info">
{{ meeting.type }}
</v-chip>
</div>
</v-col>
<v-col cols="auto">
<v-btn variant="outlined" color="primary" class="mr-2" @click="viewMeeting(meeting)">
View Details
</v-btn>
<v-btn variant="flat" color="primary" @click="joinMeeting(meeting)">
Join Meeting
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-window-item>
<!-- Past Meetings -->
<v-window-item value="past">
<v-data-table
:headers="pastMeetingHeaders"
:items="pastMeetings"
class="elevation-2"
hover
>
<template v-slot:item.title="{ item }">
<div class="font-weight-medium">{{ item.title }}</div>
</template>
<template v-slot:item.date="{ item }">
{{ formatDate(item.date) }}
</template>
<template v-slot:item.attendees="{ item }">
<v-chip size="small" variant="tonal">
{{ item.attendees }}/{{ item.totalInvited }}
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn icon="mdi-file-document" size="small" variant="text" @click="viewMinutes(item)" />
<v-btn icon="mdi-download" size="small" variant="text" @click="downloadMaterials(item)" />
</template>
</v-data-table>
</v-window-item>
<!-- Calendar View -->
<v-window-item value="calendar">
<v-card elevation="2">
<v-card-text>
<div class="text-center py-12">
<v-icon size="64" color="primary" class="mb-4">mdi-calendar-month</v-icon>
<h3 class="text-h5 mb-2">Calendar View</h3>
<p class="text-body-1 text-medium-emphasis">
Interactive calendar view coming soon
</p>
</div>
</v-card-text>
</v-card>
</v-window-item>
</v-window>
<!-- Schedule Meeting Dialog -->
<v-dialog v-model="showScheduleDialog" max-width="600">
<v-card>
<v-card-title>Schedule Board Meeting</v-card-title>
<v-card-text>
<v-form ref="meetingForm">
<v-row>
<v-col cols="12">
<v-text-field
v-model="meetingForm.title"
label="Meeting Title"
variant="outlined"
required
/>
</v-col>
<v-col cols="12">
<v-select
v-model="meetingForm.type"
label="Meeting Type"
:items="['Regular', 'Special', 'Emergency', 'Annual']"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="meetingForm.date"
label="Date"
type="date"
variant="outlined"
required
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="meetingForm.time"
label="Time"
type="time"
variant="outlined"
required
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="meetingForm.location"
label="Location"
variant="outlined"
required
/>
</v-col>
<v-col cols="12">
<v-textarea
v-model="meetingForm.agenda"
label="Agenda"
variant="outlined"
rows="3"
/>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="showScheduleDialog = false">Cancel</v-btn>
<v-btn color="primary" variant="flat" @click="scheduleMeeting">Schedule</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'board',
middleware: 'board'
});
// State
const activeTab = ref('upcoming');
const showScheduleDialog = ref(false);
// Form data
const meetingForm = ref({
title: '',
type: '',
date: '',
time: '',
location: '',
agenda: ''
});
// Mock data
const upcomingMeetings = ref([
{
id: 1,
title: 'Monthly Board Meeting - February',
date: new Date('2024-02-15'),
time: '10:00 AM',
location: 'Board Room / Zoom',
type: 'Regular',
attendees: 8
},
{
id: 2,
title: 'Strategic Planning Session',
date: new Date('2024-02-28'),
time: '2:00 PM',
location: 'Conference Center',
type: 'Special',
attendees: 12
}
]);
const pastMeetings = ref([
{
id: 3,
title: 'Monthly Board Meeting - January',
date: new Date('2024-01-15'),
time: '10:00 AM',
attendees: 9,
totalInvited: 10
},
{
id: 4,
title: 'Annual General Meeting',
date: new Date('2024-01-05'),
time: '6:00 PM',
attendees: 45,
totalInvited: 50
}
]);
// Table headers
const pastMeetingHeaders = [
{ title: 'Meeting', key: 'title' },
{ title: 'Date', key: 'date' },
{ title: 'Time', key: 'time' },
{ title: 'Attendance', key: 'attendees' },
{ title: 'Actions', key: 'actions', align: 'end' }
];
// Methods
const formatDate = (date: Date) => {
return new Date(date).toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
});
};
const viewMeeting = (meeting: any) => {
console.log('View meeting:', meeting);
};
const joinMeeting = (meeting: any) => {
console.log('Join meeting:', meeting);
};
const viewMinutes = (meeting: any) => {
console.log('View minutes:', meeting);
};
const downloadMaterials = (meeting: any) => {
console.log('Download materials:', meeting);
};
const scheduleMeeting = () => {
console.log('Schedule meeting:', meetingForm.value);
showScheduleDialog.value = false;
};
</script>

View File

@ -22,7 +22,7 @@ definePageMeta({
layout: 'dashboard'
});
const { user, userTier } = useAuth();
const { user, userTier, isAdmin, isBoard } = useAuth();
const loading = ref(true);
// Route to tier-specific dashboard - auth middleware ensures user is authenticated
@ -31,8 +31,15 @@ onMounted(() => {
// Auth middleware has already verified authentication - route based on highest privilege
if (user.value && userTier.value) {
// Use old structure for now until new pages are fully deployed
let targetRoute = `/dashboard/${userTier.value}`;
// Use new role-based structure
let targetRoute = '';
if (isAdmin.value) {
targetRoute = '/admin/dashboard';
} else if (isBoard.value) {
targetRoute = '/board/dashboard';
} else {
targetRoute = '/member/dashboard';
}
console.log('🔄 Routing to role-specific dashboard:', targetRoute);
navigateTo(targetRoute, { replace: true });