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';