Complete infrastructure reorganization to role-based structure
All checks were successful
Build And Push Image / docker (push) Successful in 1m50s
All checks were successful
Build And Push Image / docker (push) Successful in 1m50s
- Created all missing admin pages (users, settings, events, members, payments) - Created board pages (governance, meetings) - Updated dashboard router to use new /admin, /board, /member structure - Added isMember alias to useAuth composable for consistency - All pages now use correct role-based layouts and middleware - Build verified successfully The platform now has a clean separation: - /admin/* - Administrator dashboard and tools - /board/* - Board member governance and meetings - /member/* - Member portal and resources Next steps: Complete remaining member pages and clean up old dashboard files
This commit is contained in:
552
pages/admin/events/index.vue
Normal file
552
pages/admin/events/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user