fixes
Build And Push Image / docker (push) Successful in 4m16s
Details
Build And Push Image / docker (push) Successful in 4m16s
Details
This commit is contained in:
parent
db19eb2708
commit
5371ad4fa2
|
|
@ -25,14 +25,19 @@
|
|||
v-model="mobileView"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
density="comfortable"
|
||||
mandatory
|
||||
class="w-100"
|
||||
>
|
||||
<v-btn value="month">
|
||||
<v-btn value="week" class="flex-grow-1">
|
||||
<v-icon start>mdi-calendar-week</v-icon>
|
||||
Week
|
||||
</v-btn>
|
||||
<v-btn value="month" class="flex-grow-1">
|
||||
<v-icon start>mdi-calendar-month</v-icon>
|
||||
Month
|
||||
</v-btn>
|
||||
<v-btn value="list">
|
||||
<v-btn value="list" class="flex-grow-1">
|
||||
<v-icon start>mdi-format-list-bulleted</v-icon>
|
||||
Agenda
|
||||
</v-btn>
|
||||
|
|
@ -108,7 +113,7 @@ const { isBoard, isAdmin } = useAuth();
|
|||
|
||||
// Reactive state
|
||||
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
|
||||
const mobileView = ref('month');
|
||||
const mobileView = ref('week'); // Default to week view on mobile
|
||||
|
||||
// Computed properties
|
||||
const calendarHeight = computed(() => {
|
||||
|
|
@ -122,7 +127,12 @@ const currentView = computed(() => {
|
|||
|
||||
// Mobile responsive view switching
|
||||
if (process.client && window.innerWidth < 960) {
|
||||
return mobileView.value === 'list' ? 'listWeek' : 'dayGridMonth';
|
||||
switch (mobileView.value) {
|
||||
case 'week': return 'dayGridWeek';
|
||||
case 'list': return 'listWeek';
|
||||
case 'month':
|
||||
default: return 'dayGridMonth';
|
||||
}
|
||||
}
|
||||
|
||||
return props.initialView;
|
||||
|
|
@ -375,7 +385,13 @@ function gotoDate(date: string | Date) {
|
|||
|
||||
// Watch for mobile view changes
|
||||
watch(mobileView, (newView) => {
|
||||
const viewType = newView === 'list' ? 'listWeek' : 'dayGridMonth';
|
||||
let viewType;
|
||||
switch (newView) {
|
||||
case 'week': viewType = 'dayGridWeek'; break;
|
||||
case 'list': viewType = 'listWeek'; break;
|
||||
case 'month':
|
||||
default: viewType = 'dayGridMonth'; break;
|
||||
}
|
||||
changeView(viewType);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,75 +1,155 @@
|
|||
<template>
|
||||
<v-banner
|
||||
<v-card
|
||||
v-if="event"
|
||||
:color="bannerColor"
|
||||
lines="two"
|
||||
elevation="2"
|
||||
rounded
|
||||
elevation="3"
|
||||
class="upcoming-event-banner ma-2"
|
||||
:color="eventTypeColor"
|
||||
theme="dark"
|
||||
rounded="xl"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="iconColor" size="large">{{ eventIcon }}</v-icon>
|
||||
</template>
|
||||
|
||||
<template #text>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-h6 font-weight-bold mb-1">{{ event.title }}</div>
|
||||
<div class="d-flex flex-wrap align-center ga-4 text-body-2">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-calendar-clock</v-icon>
|
||||
<span>{{ formatEventDate }}</span>
|
||||
</div>
|
||||
<div v-if="event.location" class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-map-marker</v-icon>
|
||||
<span>{{ event.location }}</span>
|
||||
</div>
|
||||
<div v-if="event.is_paid === 'true'" class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-currency-eur</v-icon>
|
||||
<span>{{ memberPrice }}</span>
|
||||
</div>
|
||||
<div v-if="capacityInfo" class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-account-group</v-icon>
|
||||
<span>{{ capacityInfo }}</span>
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Mobile Layout -->
|
||||
<div v-if="$vuetify.display.mobile" class="mobile-banner-layout">
|
||||
<!-- Header -->
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-avatar :color="eventTypeColor" class="me-3" size="40">
|
||||
<v-icon :icon="eventTypeIcon" size="20"></v-icon>
|
||||
</v-avatar>
|
||||
<div class="flex-grow-1">
|
||||
<h3 class="text-h6 font-weight-bold text-truncate">{{ event.title }}</h3>
|
||||
<div class="text-caption opacity-90">{{ eventTypeLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="d-flex flex-column flex-sm-row ga-2">
|
||||
<!-- RSVP Status -->
|
||||
<v-chip
|
||||
v-if="userRSVP"
|
||||
:color="rsvpStatusColor"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
<v-icon start size="small">{{ rsvpStatusIcon }}</v-icon>
|
||||
{{ rsvpStatusText }}
|
||||
</v-chip>
|
||||
|
||||
|
||||
<!-- Event Details -->
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="16" class="me-2">mdi-calendar-clock</v-icon>
|
||||
<span class="text-body-2">{{ formatEventDate }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="event.location" class="d-flex align-center mb-1">
|
||||
<v-icon size="16" class="me-2">mdi-map-marker</v-icon>
|
||||
<span class="text-body-2 text-truncate">{{ event.location }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div v-if="event.is_paid === 'true'" class="d-flex align-center">
|
||||
<v-icon size="16" class="me-2">mdi-currency-eur</v-icon>
|
||||
<span class="text-body-2">
|
||||
€{{ memberPrice || nonMemberPrice }}
|
||||
<span v-if="memberPrice && nonMemberPrice" class="text-caption">(Members)</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="event.max_attendees" class="d-flex align-center">
|
||||
<v-icon size="16" class="me-2">mdi-account-group</v-icon>
|
||||
<span class="text-body-2">{{ event.current_attendees || 0 }}/{{ event.max_attendees }} attending</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-btn
|
||||
@click="handleViewEvent"
|
||||
variant="elevated"
|
||||
color="white"
|
||||
size="small"
|
||||
prepend-icon="mdi-eye"
|
||||
>
|
||||
View Details
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="!userRSVP && canRSVP"
|
||||
@click="handleQuickRSVP"
|
||||
:color="quickRSVPColor"
|
||||
size="small"
|
||||
prepend-icon="mdi-check"
|
||||
>
|
||||
Quick RSVP
|
||||
</v-btn>
|
||||
<div class="d-flex ga-2">
|
||||
<v-btn
|
||||
@click="handleQuickRSVP"
|
||||
:color="userRSVP ? 'success' : 'white'"
|
||||
:variant="userRSVP ? 'elevated' : 'outlined'"
|
||||
size="small"
|
||||
class="text-none flex-grow-1"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-icon start size="18">
|
||||
{{ userRSVP ? 'mdi-check' : 'mdi-plus' }}
|
||||
</v-icon>
|
||||
{{ userRSVP ? 'Attending' : 'Quick RSVP' }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@click="handleViewDetails"
|
||||
color="white"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="text-none"
|
||||
rounded="lg"
|
||||
icon
|
||||
>
|
||||
<v-icon size="18">mdi-eye</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-banner>
|
||||
|
||||
<!-- Desktop Layout -->
|
||||
<v-row v-else align="center" no-gutters>
|
||||
<v-col cols="12" md="8">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-avatar :color="eventTypeColor" class="me-3" size="32">
|
||||
<v-icon :icon="eventTypeIcon" size="16"></v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<h3 class="text-h6 font-weight-bold">{{ event.title }}</h3>
|
||||
<div class="text-caption opacity-90">{{ eventTypeLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center flex-wrap ga-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-calendar-clock</v-icon>
|
||||
<span class="text-body-2">{{ formatEventDate }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="event.location" class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-map-marker</v-icon>
|
||||
<span class="text-body-2">{{ event.location }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="event.is_paid === 'true'" class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-currency-eur</v-icon>
|
||||
<span class="text-body-2">
|
||||
€{{ memberPrice || nonMemberPrice }}
|
||||
<span v-if="memberPrice && nonMemberPrice">(Members)</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="event.max_attendees" class="d-flex align-center">
|
||||
<v-icon size="small" class="me-1">mdi-account-group</v-icon>
|
||||
<span class="text-body-2">{{ event.current_attendees || 0 }}/{{ event.max_attendees }} attending</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4" class="text-end">
|
||||
<div class="d-flex ga-2 justify-end">
|
||||
<v-btn
|
||||
@click="handleQuickRSVP"
|
||||
:color="userRSVP ? 'success' : 'white'"
|
||||
:variant="userRSVP ? 'elevated' : 'outlined'"
|
||||
size="small"
|
||||
class="text-none"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-icon start size="small">
|
||||
{{ userRSVP ? 'mdi-check' : 'mdi-plus' }}
|
||||
</v-icon>
|
||||
{{ userRSVP ? 'Attending' : 'Quick RSVP' }}
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
@click="handleViewDetails"
|
||||
color="white"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
class="text-none"
|
||||
rounded="lg"
|
||||
>
|
||||
<v-icon start size="small">mdi-eye</v-icon>
|
||||
View Details
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
@ -99,7 +179,7 @@ const canRSVP = computed(() => {
|
|||
return eventDate > now; // Can RSVP to future events
|
||||
});
|
||||
|
||||
const eventIcon = computed(() => {
|
||||
const eventTypeIcon = computed(() => {
|
||||
if (!props.event) return 'mdi-calendar';
|
||||
|
||||
const icons = {
|
||||
|
|
@ -113,7 +193,7 @@ const eventIcon = computed(() => {
|
|||
return icons[props.event.event_type as keyof typeof icons] || 'mdi-calendar';
|
||||
});
|
||||
|
||||
const bannerColor = computed(() => {
|
||||
const eventTypeColor = computed(() => {
|
||||
if (!props.event) return 'primary';
|
||||
|
||||
// Check if event is soon (within 24 hours)
|
||||
|
|
@ -137,11 +217,27 @@ const bannerColor = computed(() => {
|
|||
return colors[props.event.event_type as keyof typeof colors] || 'primary';
|
||||
});
|
||||
|
||||
const eventTypeLabel = computed(() => {
|
||||
if (!props.event) return '';
|
||||
|
||||
const labels = {
|
||||
'meeting': 'Meeting',
|
||||
'social': 'Social Event',
|
||||
'fundraiser': 'Fundraiser',
|
||||
'workshop': 'Workshop',
|
||||
'board-only': 'Board Only'
|
||||
};
|
||||
|
||||
return labels[props.event.event_type as keyof typeof labels] || 'Event';
|
||||
});
|
||||
|
||||
const iconColor = computed(() => {
|
||||
// Use white for better contrast on colored backgrounds
|
||||
return 'white';
|
||||
});
|
||||
|
||||
const nonMemberPrice = computed(() => props.event?.cost_non_members || '');
|
||||
|
||||
const formatEventDate = computed(() => {
|
||||
if (!props.event) return '';
|
||||
|
||||
|
|
@ -215,7 +311,7 @@ const rsvpStatusText = computed(() => {
|
|||
});
|
||||
|
||||
const quickRSVPColor = computed(() => {
|
||||
return bannerColor.value === 'warning' ? 'success' : 'white';
|
||||
return eventTypeColor.value === 'warning' ? 'success' : 'white';
|
||||
});
|
||||
|
||||
// Methods
|
||||
|
|
@ -225,6 +321,12 @@ const handleViewEvent = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleViewDetails = () => {
|
||||
if (props.event) {
|
||||
emit('event-click', props.event);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickRSVP = () => {
|
||||
if (props.event) {
|
||||
emit('quick-rsvp', props.event);
|
||||
|
|
|
|||
|
|
@ -135,6 +135,15 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
const newRSVP = await eventsClient.createRSVP(rsvpData);
|
||||
|
||||
// Update event attendee count
|
||||
try {
|
||||
await updateEventAttendeeCount(eventId);
|
||||
console.log('[RSVP] ✅ Updated event attendee count for event:', eventId);
|
||||
} catch (countError) {
|
||||
console.log('[RSVP] ⚠️ Failed to update attendee count:', countError);
|
||||
// Don't fail the RSVP creation if count update fails
|
||||
}
|
||||
|
||||
// Include payment information in response for paid events
|
||||
let responseData: any = newRSVP;
|
||||
|
||||
|
|
@ -191,3 +200,37 @@ async function getRegistrationConfig() {
|
|||
accountHolder: 'MonacoUSA Association'
|
||||
};
|
||||
}
|
||||
|
||||
async function updateEventAttendeeCount(eventId: string) {
|
||||
console.log('[updateEventAttendeeCount] Updating attendee count for event:', eventId);
|
||||
|
||||
try {
|
||||
const eventsClient = createNocoDBEventsClient();
|
||||
|
||||
// Get all confirmed RSVPs for this event
|
||||
const confirmedRSVPs = await eventsClient.getEventRSVPs(eventId, 'confirmed');
|
||||
|
||||
// Calculate total attendees (confirmed RSVPs + their guests)
|
||||
let totalAttendees = 0;
|
||||
|
||||
for (const rsvp of confirmedRSVPs) {
|
||||
totalAttendees += 1; // The member themselves
|
||||
const guestCount = parseInt(rsvp.extra_guests || '0');
|
||||
totalAttendees += guestCount; // Add their guests
|
||||
}
|
||||
|
||||
console.log('[updateEventAttendeeCount] Calculated total attendees:', totalAttendees, 'from', confirmedRSVPs.length, 'RSVPs');
|
||||
|
||||
// Update the event's current_attendees field
|
||||
await eventsClient.update(eventId, {
|
||||
current_attendees: totalAttendees.toString()
|
||||
});
|
||||
|
||||
console.log('[updateEventAttendeeCount] ✅ Successfully updated event attendee count to:', totalAttendees);
|
||||
|
||||
return totalAttendees;
|
||||
} catch (error) {
|
||||
console.error('[updateEventAttendeeCount] ❌ Error updating attendee count:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,6 +253,23 @@ export function createNocoDBEventsClient() {
|
|||
// Apply field normalization like members system
|
||||
if (result.list) {
|
||||
result.list = result.list.map(normalizeEventFieldsFromNocoDB);
|
||||
|
||||
// Update attendee counts for all events
|
||||
result.list = await Promise.all(
|
||||
result.list.map(async (event) => {
|
||||
try {
|
||||
const eventId = event.event_id || event.id || (event as any).Id;
|
||||
const updatedCount = await this.calculateEventAttendeeCount(eventId.toString());
|
||||
return {
|
||||
...event,
|
||||
current_attendees: updatedCount.toString()
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('[nocodb-events] ⚠️ Failed to calculate attendee count for event:', event.title, error);
|
||||
return event; // Return original if calculation fails
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -490,6 +507,7 @@ export function createNocoDBEventsClient() {
|
|||
payment_reference: rsvpData.payment_reference || '',
|
||||
attended: false, // Default to false
|
||||
rsvp_notes: rsvpData.rsvp_notes || '',
|
||||
extra_guests: rsvpData.extra_guests || '0', // Include guest count
|
||||
is_member_pricing: rsvpData.is_member_pricing === 'true' || rsvpData.is_member_pricing === true
|
||||
};
|
||||
|
||||
|
|
@ -651,6 +669,74 @@ export function createNocoDBEventsClient() {
|
|||
console.error('[nocodb-events] ❌ Error updating RSVP:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all RSVPs for an event with optional status filter
|
||||
*/
|
||||
async getEventRSVPs(eventId: string, rsvpStatus?: string) {
|
||||
console.log('[nocodb-events] Getting RSVPs for event:', eventId, 'with status:', rsvpStatus);
|
||||
|
||||
try {
|
||||
try {
|
||||
// Build where clause
|
||||
let whereClause = `(event_id,eq,${eventId})`;
|
||||
if (rsvpStatus) {
|
||||
whereClause += `~and(rsvp_status,eq,${rsvpStatus})`;
|
||||
}
|
||||
|
||||
// Try to get from RSVP table first
|
||||
const rsvps = await $fetch<{list: any[]}>(createEventTableUrl(EventTable.EventRSVPs), {
|
||||
headers: {
|
||||
"xc-token": getNocoDbConfiguration().token,
|
||||
},
|
||||
params: {
|
||||
where: whereClause,
|
||||
limit: 1000 // High limit to get all RSVPs
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[nocodb-events] ✅ Found', rsvps.list?.length || 0, 'RSVPs for event:', eventId);
|
||||
return rsvps.list || [];
|
||||
|
||||
} catch (rsvpTableError: any) {
|
||||
console.log('[nocodb-events] ⚠️ RSVP table not available, returning empty array');
|
||||
|
||||
// Return empty array if table not available
|
||||
return [];
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[nocodb-events] ❌ Error getting event RSVPs:', error);
|
||||
return []; // Return empty array instead of throwing to prevent blocking
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate total attendee count for an event (confirmed RSVPs + their guests)
|
||||
*/
|
||||
async calculateEventAttendeeCount(eventId: string): Promise<number> {
|
||||
console.log('[nocodb-events] Calculating attendee count for event:', eventId);
|
||||
|
||||
try {
|
||||
// Get all confirmed RSVPs for this event
|
||||
const confirmedRSVPs = await this.getEventRSVPs(eventId, 'confirmed');
|
||||
|
||||
// Calculate total attendees (confirmed RSVPs + their guests)
|
||||
let totalAttendees = 0;
|
||||
|
||||
for (const rsvp of confirmedRSVPs) {
|
||||
totalAttendees += 1; // The member themselves
|
||||
const guestCount = parseInt(rsvp.extra_guests || '0');
|
||||
totalAttendees += guestCount; // Add their guests
|
||||
}
|
||||
|
||||
console.log('[nocodb-events] ✅ Calculated total attendees:', totalAttendees, 'from', confirmedRSVPs.length, 'confirmed RSVPs');
|
||||
|
||||
return totalAttendees;
|
||||
} catch (error) {
|
||||
console.error('[nocodb-events] ❌ Error calculating attendee count for event:', eventId, error);
|
||||
return 0; // Return 0 if calculation fails
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue