From d215dfedc7e9bf818de2e34941e2792b38ce70f6 Mon Sep 17 00:00:00 2001
From: Matt
Date: Wed, 13 Aug 2025 13:51:27 +0200
Subject: [PATCH] db updates and fixes
---
components/DuesActionCard.vue | 127 +++++++++++++++++---
components/EventCalendar.vue | 15 ++-
server/api/admin/backfill-event-ids.post.ts | 104 ++++++++++++++++
server/utils/nocodb-events.ts | 23 +++-
utils/types.ts | 1 +
5 files changed, 249 insertions(+), 21 deletions(-)
create mode 100644 server/api/admin/backfill-event-ids.post.ts
diff --git a/components/DuesActionCard.vue b/components/DuesActionCard.vue
index 311b34b..b2e0e06 100644
--- a/components/DuesActionCard.vue
+++ b/components/DuesActionCard.vue
@@ -89,7 +89,7 @@
Due Date
- {{ formatDate(member.nextDueDate || member.payment_due_date) }}
+ {{ formatDate(member.nextDueDate || member.payment_due_date || '') }}
@@ -151,17 +151,27 @@
-
+
+
+
+
+ Select the date when the payment was received
+
+
(), {
@@ -259,14 +278,24 @@ const emit = defineEmits();
// Reactive state for payment date dialog
const showPaymentDateDialog = ref(false);
const selectedPaymentDate = ref('');
+const selectedPaymentModel = ref(null);
// Initialize with today's date when dialog opens
watch(showPaymentDateDialog, (isOpen) => {
if (isOpen) {
+ const today = new Date();
+ selectedPaymentModel.value = today;
selectedPaymentDate.value = todayDate.value;
}
});
+// Date picker handler
+const handleDateUpdate = (date: Date | null) => {
+ if (date) {
+ selectedPaymentDate.value = date.toISOString().split('T')[0];
+ }
+};
+
// Computed properties
const memberInitials = computed(() => {
const firstName = props.member.first_name || '';
@@ -458,6 +487,76 @@ const confirmMarkAsPaid = async () => {
max-width: 150px;
}
+/* Date picker styling to match Vuetify */
+.date-picker-wrapper {
+ width: 100%;
+}
+
+.date-picker-label {
+ font-size: 16px;
+ color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
+ font-weight: 400;
+ line-height: 1.5;
+ letter-spacing: 0.009375em;
+ margin-bottom: 8px;
+ display: block;
+}
+
+/* Style the Vue DatePicker to match Vuetify inputs */
+:deep(.dp__input) {
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 4px;
+ padding: 16px 12px;
+ padding-right: 48px; /* Make room for calendar icon */
+ font-size: 16px;
+ line-height: 1.5;
+ background: rgb(var(--v-theme-surface));
+ color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
+ transition: border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ width: 100%;
+ min-height: 56px;
+}
+
+:deep(.dp__input:hover) {
+ border-color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
+}
+
+:deep(.dp__input:focus) {
+ border-color: rgb(var(--v-theme-primary));
+ border-width: 2px;
+ outline: none;
+}
+
+:deep(.dp__input_readonly) {
+ cursor: pointer;
+}
+
+/* Style the date picker dropdown */
+:deep(.dp__menu) {
+ border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
+ border-radius: 4px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ background: rgb(var(--v-theme-surface));
+}
+
+/* Primary color theming for the date picker */
+:deep(.dp__primary_color) {
+ background-color: rgb(var(--v-theme-primary));
+}
+
+:deep(.dp__primary_text) {
+ color: rgb(var(--v-theme-primary));
+}
+
+:deep(.dp__active_date) {
+ background-color: rgb(var(--v-theme-primary));
+ color: rgb(var(--v-theme-on-primary));
+}
+
+:deep(.dp__today) {
+ border: 1px solid rgb(var(--v-theme-primary));
+}
+
/* Mobile responsive */
@media (max-width: 600px) {
.dues-action-card {
diff --git a/components/EventCalendar.vue b/components/EventCalendar.vue
index 70f59db..837c8b9 100644
--- a/components/EventCalendar.vue
+++ b/components/EventCalendar.vue
@@ -260,6 +260,7 @@ function handleEventMount(mountInfo: any) {
function transformEventForCalendar(event: Event): FullCalendarEvent {
console.log('[EventCalendar] Transforming event:', {
id: event.id,
+ event_id: event.event_id,
title: event.title,
start_datetime: event.start_datetime,
end_datetime: event.end_datetime,
@@ -277,6 +278,10 @@ function transformEventForCalendar(event: Event): FullCalendarEvent {
const colors = eventTypeColors[event.event_type] ||
{ bg: '#757575', border: '#424242' };
+ // Use event_id as the primary identifier for FullCalendar uniqueness
+ const calendarId = event.event_id || event.id || `temp_${(event as any).Id}_${Date.now()}`;
+ console.log('[EventCalendar] Using calendar ID:', calendarId, 'from event_id:', event.event_id, 'fallback id:', event.id);
+
// Ensure dates are properly formatted for FullCalendar
let startDate: string | Date;
let endDate: string | Date;
@@ -287,7 +292,7 @@ function transformEventForCalendar(event: Event): FullCalendarEvent {
const endDateObj = new Date(event.end_datetime);
if (isNaN(startDateObj.getTime()) || isNaN(endDateObj.getTime())) {
- console.error('[EventCalendar] Invalid date values for event:', event.id, {
+ console.error('[EventCalendar] Invalid date values for event:', calendarId, {
start: event.start_datetime,
end: event.end_datetime
});
@@ -299,14 +304,14 @@ function transformEventForCalendar(event: Event): FullCalendarEvent {
endDate = endDateObj.toISOString();
}
} catch (error) {
- console.error('[EventCalendar] Date parsing error for event:', event.id, error);
+ console.error('[EventCalendar] Date parsing error for event:', calendarId, error);
// Use fallback dates
startDate = new Date().toISOString();
endDate = new Date(Date.now() + 3600000).toISOString(); // +1 hour
}
const transformedEvent = {
- id: event.id,
+ id: calendarId, // ✅ Use event_id instead of event.id
title: event.title,
start: startDate,
end: endDate,
@@ -325,7 +330,9 @@ function transformEventForCalendar(event: Event): FullCalendarEvent {
current_attendees: typeof event.current_attendees === 'string' ? parseInt(event.current_attendees) : (event.current_attendees || 0),
user_rsvp: event.user_rsvp,
visibility: event.visibility,
- creator: event.creator
+ creator: event.creator,
+ event_id: event.event_id, // Store for reference
+ database_id: event.id || (event as any).Id
}
};
diff --git a/server/api/admin/backfill-event-ids.post.ts b/server/api/admin/backfill-event-ids.post.ts
new file mode 100644
index 0000000..78fc8b3
--- /dev/null
+++ b/server/api/admin/backfill-event-ids.post.ts
@@ -0,0 +1,104 @@
+// server/api/admin/backfill-event-ids.post.ts
+export default defineEventHandler(async (event) => {
+ console.log('[admin/backfill-event-ids] Starting event_id backfill process...');
+
+ try {
+ // Verify admin access
+ const sessionManager = createSessionManager();
+ const cookieHeader = getHeader(event, 'cookie');
+ const session = sessionManager.getSession(cookieHeader);
+
+ if (!session || session.user.tier !== 'admin') {
+ throw createError({
+ statusCode: 403,
+ statusMessage: 'Admin access required'
+ });
+ }
+
+ console.log(`[admin/backfill-event-ids] Admin access verified for user: ${session.user.email}`);
+
+ const { createNocoDBEventsClient } = await import('~/server/utils/nocodb-events');
+ const eventsClient = createNocoDBEventsClient();
+
+ // Get all events
+ const response = await eventsClient.findAll({ limit: 1000 });
+ const events = response.list || [];
+
+ console.log(`[admin/backfill-event-ids] Found ${events.length} events to process`);
+
+ const results = {
+ processed: 0,
+ updated: 0,
+ skipped: 0,
+ errors: 0,
+ details: [] as any[]
+ };
+
+ for (const eventItem of events) {
+ results.processed++;
+ const eventId = (eventItem as any).Id;
+
+ try {
+ // Check if event_id already exists
+ if (eventItem.event_id && eventItem.event_id.trim() !== '') {
+ console.log(`[admin/backfill-event-ids] Event ${eventId} already has event_id: ${eventItem.event_id}`);
+ results.skipped++;
+ results.details.push({
+ id: eventId,
+ title: eventItem.title,
+ status: 'skipped',
+ reason: 'Already has event_id',
+ existing_event_id: eventItem.event_id
+ });
+ continue;
+ }
+
+ // Generate unique event_id based on event date and title
+ const eventDate = new Date(eventItem.start_datetime);
+ const dateString = eventDate.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD
+ const timeString = eventDate.toISOString().split('T')[1].split(':').slice(0,2).join(''); // HHMM
+ const titleSlug = eventItem.title.toLowerCase().replace(/[^a-z0-9]/g, '').substring(0, 8);
+
+ const newEventId = `evt_${dateString}_${timeString}_${titleSlug}`;
+
+ console.log(`[admin/backfill-event-ids] Updating event ${eventId} (${eventItem.title}) with event_id: ${newEventId}`);
+
+ // Update the event with the new event_id
+ await eventsClient.update(eventId.toString(), { event_id: newEventId });
+
+ results.updated++;
+ results.details.push({
+ id: eventId,
+ title: eventItem.title,
+ status: 'updated',
+ new_event_id: newEventId
+ });
+
+ } catch (updateError: any) {
+ console.error(`[admin/backfill-event-ids] Error updating event ${eventId}:`, updateError);
+ results.errors++;
+ results.details.push({
+ id: eventId,
+ title: eventItem.title,
+ status: 'error',
+ error: updateError.message || 'Update failed'
+ });
+ }
+ }
+
+ console.log(`[admin/backfill-event-ids] Backfill completed. Processed: ${results.processed}, Updated: ${results.updated}, Skipped: ${results.skipped}, Errors: ${results.errors}`);
+
+ return {
+ success: true,
+ message: `Event ID backfill completed successfully`,
+ data: results
+ };
+
+ } catch (error: any) {
+ console.error('[admin/backfill-event-ids] Backfill process failed:', error);
+ throw createError({
+ statusCode: 500,
+ statusMessage: error.message || 'Event ID backfill failed'
+ });
+ }
+});
diff --git a/server/utils/nocodb-events.ts b/server/utils/nocodb-events.ts
index 205d5a3..85bdf5a 100644
--- a/server/utils/nocodb-events.ts
+++ b/server/utils/nocodb-events.ts
@@ -92,6 +92,7 @@ export const normalizeEventFieldsFromNocoDB = (data: any): Event => {
'Title': 'title',
'Description': 'description',
'Event Type': 'event_type',
+ 'Event ID': 'event_id',
'Start Date': 'start_datetime',
'End Date': 'end_datetime',
'Location': 'location',
@@ -106,6 +107,7 @@ export const normalizeEventFieldsFromNocoDB = (data: any): Event => {
'title': 'title',
'description': 'description',
'event_type': 'event_type',
+ 'event_id': 'event_id',
'start_datetime': 'start_datetime',
'end_datetime': 'end_datetime',
'location': 'location',
@@ -257,7 +259,7 @@ export function createNocoDBEventsClient() {
// Only include allowed event fields
const allowedFields = [
- "title", "description", "event_type", "start_datetime", "end_datetime",
+ "title", "description", "event_type", "event_id", "start_datetime", "end_datetime",
"location", "is_recurring", "recurrence_pattern", "max_attendees",
"is_paid", "cost_members", "cost_non_members", "member_pricing_enabled",
"visibility", "status", "creator", "current_attendees"
@@ -269,6 +271,14 @@ export function createNocoDBEventsClient() {
}
}
+ // Generate unique event_id if not provided
+ if (!cleanData.event_id) {
+ const timestamp = Date.now();
+ const randomString = Math.random().toString(36).substring(2, 8);
+ cleanData.event_id = `evt_${timestamp}_${randomString}`;
+ console.log('[nocodb-events] Generated event_id:', cleanData.event_id);
+ }
+
// Set defaults
cleanData.status = cleanData.status || 'active';
cleanData.visibility = cleanData.visibility || 'public';
@@ -411,8 +421,13 @@ export function transformEventForCalendar(event: Event): any {
const colors = eventTypeColors[event.event_type as keyof typeof eventTypeColors] ||
{ bg: '#757575', border: '#424242' };
+ // Use event_id as the primary identifier for FullCalendar
+ const calendarId = event.event_id || event.id || `temp_${(event as any).Id}_${Date.now()}`;
+
+ console.log('[transformEventForCalendar] Event:', event.title, 'ID:', calendarId, 'event_id:', event.event_id, 'system id:', event.id);
+
return {
- id: event.id,
+ id: calendarId,
title: event.title,
start: event.start_datetime,
end: event.end_datetime,
@@ -431,7 +446,9 @@ export function transformEventForCalendar(event: Event): any {
user_rsvp: event.user_rsvp,
visibility: event.visibility,
creator: event.creator,
- status: event.status
+ status: event.status,
+ event_id: event.event_id,
+ database_id: event.id || (event as any).Id
}
};
}
diff --git a/utils/types.ts b/utils/types.ts
index b54d157..0a60dc9 100644
--- a/utils/types.ts
+++ b/utils/types.ts
@@ -441,6 +441,7 @@ export interface DuesCalculationUtils {
// Event Management System Types
export interface Event {
id: string;
+ event_id?: string; // Custom event identifier (e.g., "evt_1723555200_abc123")
title: string;
description: string;
event_type: 'meeting' | 'social' | 'fundraiser' | 'workshop' | 'board-only';