db updates and fixes
Build And Push Image / docker (push) Successful in 3m59s
Details
Build And Push Image / docker (push) Successful in 3m59s
Details
This commit is contained in:
parent
a0153a76a4
commit
d215dfedc7
|
|
@ -89,7 +89,7 @@
|
||||||
Due Date
|
Due Date
|
||||||
</span>
|
</span>
|
||||||
<span class="text-body-2 font-weight-bold text-warning">
|
<span class="text-body-2 font-weight-bold text-warning">
|
||||||
{{ formatDate(member.nextDueDate || member.payment_due_date) }}
|
{{ formatDate(member.nextDueDate || member.payment_due_date || '') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -151,17 +151,27 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-text-field
|
<div class="date-picker-wrapper">
|
||||||
v-model="selectedPaymentDate"
|
<label class="date-picker-label">Payment Date</label>
|
||||||
label="Payment Date"
|
<VueDatePicker
|
||||||
type="date"
|
v-model="selectedPaymentModel"
|
||||||
variant="outlined"
|
:timezone="{
|
||||||
:max="todayDate"
|
timezone: 'Europe/Monaco',
|
||||||
prepend-inner-icon="mdi-calendar"
|
emitTimezone: 'UTC'
|
||||||
hint="Select the date when the payment was received"
|
}"
|
||||||
persistent-hint
|
:format="'dd/MM/yyyy (Monaco)'"
|
||||||
class="mb-2"
|
:max-date="new Date()"
|
||||||
/>
|
placeholder="Select payment date"
|
||||||
|
:enable-time-picker="false"
|
||||||
|
auto-apply
|
||||||
|
:clearable="false"
|
||||||
|
:required="true"
|
||||||
|
@update:model-value="handleDateUpdate"
|
||||||
|
/>
|
||||||
|
<div class="text-caption text-medium-emphasis mt-1">
|
||||||
|
Select the date when the payment was received
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-alert
|
<v-alert
|
||||||
v-if="selectedPaymentDate && isDateInFuture"
|
v-if="selectedPaymentDate && isDateInFuture"
|
||||||
|
|
@ -229,7 +239,16 @@
|
||||||
import type { Member } from '~/utils/types';
|
import type { Member } from '~/utils/types';
|
||||||
|
|
||||||
// Extended member type for dues management
|
// Extended member type for dues management
|
||||||
interface DuesMember extends Member {
|
interface DuesMember {
|
||||||
|
Id: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
nationality?: string;
|
||||||
|
member_id?: string;
|
||||||
|
FullName?: string;
|
||||||
|
FormattedPhone?: string;
|
||||||
overdueDays?: number;
|
overdueDays?: number;
|
||||||
overdueReason?: string;
|
overdueReason?: string;
|
||||||
daysUntilDue?: number;
|
daysUntilDue?: number;
|
||||||
|
|
@ -247,7 +266,7 @@ interface Props {
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'mark-paid', member: Member): void;
|
(e: 'mark-paid', member: Member): void;
|
||||||
(e: 'view-member', member: Member): void;
|
(e: 'view-member', member: DuesMember): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -259,14 +278,24 @@ const emit = defineEmits<Emits>();
|
||||||
// Reactive state for payment date dialog
|
// Reactive state for payment date dialog
|
||||||
const showPaymentDateDialog = ref(false);
|
const showPaymentDateDialog = ref(false);
|
||||||
const selectedPaymentDate = ref('');
|
const selectedPaymentDate = ref('');
|
||||||
|
const selectedPaymentModel = ref<Date | null>(null);
|
||||||
|
|
||||||
// Initialize with today's date when dialog opens
|
// Initialize with today's date when dialog opens
|
||||||
watch(showPaymentDateDialog, (isOpen) => {
|
watch(showPaymentDateDialog, (isOpen) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
const today = new Date();
|
||||||
|
selectedPaymentModel.value = today;
|
||||||
selectedPaymentDate.value = todayDate.value;
|
selectedPaymentDate.value = todayDate.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Date picker handler
|
||||||
|
const handleDateUpdate = (date: Date | null) => {
|
||||||
|
if (date) {
|
||||||
|
selectedPaymentDate.value = date.toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const memberInitials = computed(() => {
|
const memberInitials = computed(() => {
|
||||||
const firstName = props.member.first_name || '';
|
const firstName = props.member.first_name || '';
|
||||||
|
|
@ -458,6 +487,76 @@ const confirmMarkAsPaid = async () => {
|
||||||
max-width: 150px;
|
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 */
|
/* Mobile responsive */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.dues-action-card {
|
.dues-action-card {
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,7 @@ function handleEventMount(mountInfo: any) {
|
||||||
function transformEventForCalendar(event: Event): FullCalendarEvent {
|
function transformEventForCalendar(event: Event): FullCalendarEvent {
|
||||||
console.log('[EventCalendar] Transforming event:', {
|
console.log('[EventCalendar] Transforming event:', {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
event_id: event.event_id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
start_datetime: event.start_datetime,
|
start_datetime: event.start_datetime,
|
||||||
end_datetime: event.end_datetime,
|
end_datetime: event.end_datetime,
|
||||||
|
|
@ -277,6 +278,10 @@ function transformEventForCalendar(event: Event): FullCalendarEvent {
|
||||||
const colors = eventTypeColors[event.event_type] ||
|
const colors = eventTypeColors[event.event_type] ||
|
||||||
{ bg: '#757575', border: '#424242' };
|
{ 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
|
// Ensure dates are properly formatted for FullCalendar
|
||||||
let startDate: string | Date;
|
let startDate: string | Date;
|
||||||
let endDate: string | Date;
|
let endDate: string | Date;
|
||||||
|
|
@ -287,7 +292,7 @@ function transformEventForCalendar(event: Event): FullCalendarEvent {
|
||||||
const endDateObj = new Date(event.end_datetime);
|
const endDateObj = new Date(event.end_datetime);
|
||||||
|
|
||||||
if (isNaN(startDateObj.getTime()) || isNaN(endDateObj.getTime())) {
|
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,
|
start: event.start_datetime,
|
||||||
end: event.end_datetime
|
end: event.end_datetime
|
||||||
});
|
});
|
||||||
|
|
@ -299,14 +304,14 @@ function transformEventForCalendar(event: Event): FullCalendarEvent {
|
||||||
endDate = endDateObj.toISOString();
|
endDate = endDateObj.toISOString();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
// Use fallback dates
|
||||||
startDate = new Date().toISOString();
|
startDate = new Date().toISOString();
|
||||||
endDate = new Date(Date.now() + 3600000).toISOString(); // +1 hour
|
endDate = new Date(Date.now() + 3600000).toISOString(); // +1 hour
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformedEvent = {
|
const transformedEvent = {
|
||||||
id: event.id,
|
id: calendarId, // ✅ Use event_id instead of event.id
|
||||||
title: event.title,
|
title: event.title,
|
||||||
start: startDate,
|
start: startDate,
|
||||||
end: endDate,
|
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),
|
current_attendees: typeof event.current_attendees === 'string' ? parseInt(event.current_attendees) : (event.current_attendees || 0),
|
||||||
user_rsvp: event.user_rsvp,
|
user_rsvp: event.user_rsvp,
|
||||||
visibility: event.visibility,
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -92,6 +92,7 @@ export const normalizeEventFieldsFromNocoDB = (data: any): Event => {
|
||||||
'Title': 'title',
|
'Title': 'title',
|
||||||
'Description': 'description',
|
'Description': 'description',
|
||||||
'Event Type': 'event_type',
|
'Event Type': 'event_type',
|
||||||
|
'Event ID': 'event_id',
|
||||||
'Start Date': 'start_datetime',
|
'Start Date': 'start_datetime',
|
||||||
'End Date': 'end_datetime',
|
'End Date': 'end_datetime',
|
||||||
'Location': 'location',
|
'Location': 'location',
|
||||||
|
|
@ -106,6 +107,7 @@ export const normalizeEventFieldsFromNocoDB = (data: any): Event => {
|
||||||
'title': 'title',
|
'title': 'title',
|
||||||
'description': 'description',
|
'description': 'description',
|
||||||
'event_type': 'event_type',
|
'event_type': 'event_type',
|
||||||
|
'event_id': 'event_id',
|
||||||
'start_datetime': 'start_datetime',
|
'start_datetime': 'start_datetime',
|
||||||
'end_datetime': 'end_datetime',
|
'end_datetime': 'end_datetime',
|
||||||
'location': 'location',
|
'location': 'location',
|
||||||
|
|
@ -257,7 +259,7 @@ export function createNocoDBEventsClient() {
|
||||||
|
|
||||||
// Only include allowed event fields
|
// Only include allowed event fields
|
||||||
const allowedFields = [
|
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",
|
"location", "is_recurring", "recurrence_pattern", "max_attendees",
|
||||||
"is_paid", "cost_members", "cost_non_members", "member_pricing_enabled",
|
"is_paid", "cost_members", "cost_non_members", "member_pricing_enabled",
|
||||||
"visibility", "status", "creator", "current_attendees"
|
"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
|
// Set defaults
|
||||||
cleanData.status = cleanData.status || 'active';
|
cleanData.status = cleanData.status || 'active';
|
||||||
cleanData.visibility = cleanData.visibility || 'public';
|
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] ||
|
const colors = eventTypeColors[event.event_type as keyof typeof eventTypeColors] ||
|
||||||
{ bg: '#757575', border: '#424242' };
|
{ 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 {
|
return {
|
||||||
id: event.id,
|
id: calendarId,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
start: event.start_datetime,
|
start: event.start_datetime,
|
||||||
end: event.end_datetime,
|
end: event.end_datetime,
|
||||||
|
|
@ -431,7 +446,9 @@ export function transformEventForCalendar(event: Event): any {
|
||||||
user_rsvp: event.user_rsvp,
|
user_rsvp: event.user_rsvp,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
creator: event.creator,
|
creator: event.creator,
|
||||||
status: event.status
|
status: event.status,
|
||||||
|
event_id: event.event_id,
|
||||||
|
database_id: event.id || (event as any).Id
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,7 @@ export interface DuesCalculationUtils {
|
||||||
// Event Management System Types
|
// Event Management System Types
|
||||||
export interface Event {
|
export interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
|
event_id?: string; // Custom event identifier (e.g., "evt_1723555200_abc123")
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
event_type: 'meeting' | 'social' | 'fundraiser' | 'workshop' | 'board-only';
|
event_type: 'meeting' | 'social' | 'fundraiser' | 'workshop' | 'board-only';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue