Complete infrastructure reorganization to role-based structure
Build And Push Image / docker (push) Successful in 1m50s
Details
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:
parent
7c49b9db66
commit
9fa9db9b8a
|
|
@ -42,6 +42,9 @@ export const useAuth = () => {
|
|||
return user.value?.tier === 'user';
|
||||
});
|
||||
|
||||
// Alias for consistency with new naming convention
|
||||
const isMember = isUser;
|
||||
|
||||
const isBoard = computed(() => {
|
||||
// Check new realm roles first
|
||||
if (hasRole('monaco-board')) return true;
|
||||
|
|
@ -300,6 +303,7 @@ export const useAuth = () => {
|
|||
// Tier-based properties
|
||||
userTier,
|
||||
isUser,
|
||||
isMember, // Alias for isUser, better naming convention
|
||||
isBoard,
|
||||
isAdmin,
|
||||
firstName,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue