import type { Member, MembershipStatus, MemberResponse, NocoDBSettings } from '~/utils/types'; // Data normalization functions export const normalizePersonName = (name: string): string => { if (!name) return 'Unknown'; // Trim whitespace and normalize case return name.trim() .split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); }; // Pagination interface export interface PageInfo { pageSize: number; totalRows: number; isFirstPage: boolean; isLastPage: boolean; page: number; } // Response interfaces export interface EntityResponse { list: T[]; PageInfo: PageInfo; } // Dynamic table ID getter - will use configured table ID from admin panel export const getTableId = (tableName: 'Members'): string => { // Try to get table ID from global configuration first if (globalNocoDBConfig?.tables && tableName === 'Members') { const tableId = globalNocoDBConfig.tables['members'] || globalNocoDBConfig.tables['Members']; if (tableId) { console.log(`[nocodb] Using global table ID for ${tableName}:`, tableId); return tableId; } } // Fallback to default const defaultTableId = 'members-table-id'; console.log(`[nocodb] Using fallback table ID for ${tableName}:`, defaultTableId); return defaultTableId; }; // Table ID enumeration - Replace with your actual table IDs export enum Table { Members = "members-table-id", // Will be configured via admin panel } /** * Convert date from DD-MM-YYYY format to YYYY-MM-DD format for PostgreSQL */ const convertDateFormat = (dateString: string): string => { if (!dateString) return dateString; // If it's already in ISO format or contains 'T', return as is if (dateString.includes('T') || dateString.match(/^\d{4}-\d{2}-\d{2}/)) { return dateString; } // Handle DD-MM-YYYY format const ddmmyyyyMatch = dateString.match(/^(\d{1,2})-(\d{1,2})-(\d{4})$/); if (ddmmyyyyMatch) { const [, day, month, year] = ddmmyyyyMatch; const convertedDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; console.log(`[convertDateFormat] Converted ${dateString} to ${convertedDate}`); return convertedDate; } // Handle DD/MM/YYYY format const ddmmyyyySlashMatch = dateString.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (ddmmyyyySlashMatch) { const [, day, month, year] = ddmmyyyySlashMatch; const convertedDate = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`; console.log(`[convertDateFormat] Converted ${dateString} to ${convertedDate}`); return convertedDate; } console.warn(`[convertDateFormat] Could not parse date format: ${dateString}`); return dateString; }; // String data handling functions export const parseStringBoolean = (value: string): boolean => { return value === 'true'; }; export const formatBooleanAsString = (value: boolean): string => { return value ? 'true' : 'false'; }; export const parseNationalities = (nationalityString: string): string[] => { return nationalityString ? nationalityString.split(',').map(n => n.trim()).filter(n => n.length > 0) : []; }; export const formatNationalitiesAsString = (nationalities: string[]): string => { return nationalities.filter(n => n && n.trim()).join(','); }; // Field normalization functions export const normalizeFieldsFromNocoDB = (data: any): Member => { console.log('[normalizeFieldsFromNocoDB] Input data keys:', Object.keys(data)); const normalized: any = { ...data }; // Field mapping for display names to snake_case (READ operations) const readFieldMap: Record = { 'First Name': 'first_name', 'Last Name': 'last_name', 'Email': 'email', 'Email Address': 'email', 'Phone': 'phone', 'Phone Number': 'phone', 'Date of Birth': 'date_of_birth', 'Nationality': 'nationality', 'Address': 'address', 'Membership Status': 'membership_status', 'Member Since': 'member_since', 'Current Year Dues Paid': 'current_year_dues_paid', 'Membership Date Paid': 'membership_date_paid', 'Payment Due Date': 'payment_due_date', 'Keycloak ID': 'keycloak_id', 'keycloak_id': 'keycloak_id', // Also handle reverse mapping in case data comes in snake_case already 'first_name': 'first_name', 'last_name': 'last_name', 'email': 'email', 'phone': 'phone', 'date_of_birth': 'date_of_birth', 'nationality': 'nationality', 'address': 'address', 'membership_status': 'membership_status', 'member_since': 'member_since', 'current_year_dues_paid': 'current_year_dues_paid', 'membership_date_paid': 'membership_date_paid', 'payment_due_date': 'payment_due_date' }; // Apply field mapping for (const [sourceKey, targetKey] of Object.entries(readFieldMap)) { if (sourceKey in data && data[sourceKey] !== undefined && data[sourceKey] !== null) { normalized[targetKey] = data[sourceKey]; console.log(`[normalizeFieldsFromNocoDB] Mapped "${sourceKey}" -> "${targetKey}":`, data[sourceKey]); } } // Ensure required fields exist with fallbacks normalized.first_name = normalized.first_name || normalized['First Name'] || ''; normalized.last_name = normalized.last_name || normalized['Last Name'] || ''; normalized.email = normalized.email || normalized['Email'] || normalized['Email Address'] || ''; console.log('[normalizeFieldsFromNocoDB] Normalized member fields:', Object.keys(normalized)); console.log('[normalizeFieldsFromNocoDB] Final first_name:', normalized.first_name); console.log('[normalizeFieldsFromNocoDB] Final last_name:', normalized.last_name); return normalized as Member; }; export const normalizeFieldsForNocoDB = (data: any): Record => { console.log('[normalizeFieldsForNocoDB] Input data keys:', Object.keys(data)); // Field mapping for snake_case to display names (WRITE operations) const writeFieldMap: Record = { 'first_name': 'First Name', 'last_name': 'Last Name', 'email': 'Email', 'phone': 'Phone', 'date_of_birth': 'Date of Birth', 'nationality': 'Nationality', 'address': 'Address', 'membership_status': 'Membership Status', 'member_since': 'Member Since', 'current_year_dues_paid': 'Current Year Dues Paid', 'membership_date_paid': 'Membership Date Paid', 'payment_due_date': 'Payment Due Date', 'keycloak_id': 'Keycloak ID' }; const normalized: any = {}; // First, try direct mapping using write field map for (const [sourceKey, targetKey] of Object.entries(writeFieldMap)) { if (sourceKey in data && data[sourceKey] !== undefined) { normalized[targetKey] = data[sourceKey]; console.log(`[normalizeFieldsForNocoDB] Mapped "${sourceKey}" -> "${targetKey}":`, data[sourceKey]); } } // If no mappings worked, try to use the data as-is (fallback) if (Object.keys(normalized).length === 0) { console.log('[normalizeFieldsForNocoDB] No field mappings applied, using original data'); return { ...data }; } console.log('[normalizeFieldsForNocoDB] Final normalized keys:', Object.keys(normalized)); return normalized; }; // Global variable to store effective configuration let globalNocoDBConfig: any = null; // Function to set the global configuration (called by the admin-config system) export const setGlobalNocoDBConfig = (config: any) => { globalNocoDBConfig = config; console.log('[nocodb] Global configuration updated:', config.url); }; export const getNocoDbConfiguration = () => { // Try to use the global configuration first if (globalNocoDBConfig) { console.log('[nocodb] Using global configuration - URL:', globalNocoDBConfig.url); return { url: globalNocoDBConfig.url, token: globalNocoDBConfig.token, baseId: globalNocoDBConfig.baseId }; } // Fallback to runtime config console.log('[nocodb] Global config not available, using runtime config'); const config = useRuntimeConfig().nocodb; const fallbackConfig = { ...config, url: config.url || 'https://database.monacousa.org' }; console.log('[nocodb] Fallback configuration URL:', fallbackConfig.url); return fallbackConfig; }; export const createTableUrl = (table: Table | string) => { let tableId: string; if (table === Table.Members || table === 'Members') { tableId = getTableId('Members'); } else { tableId = table.toString(); } const url = `${getNocoDbConfiguration().url}/api/v2/tables/${tableId}/records`; console.log('[nocodb] Table URL:', url); return url; }; // CRUD operations for Members table export const getMembers = async (): Promise> => { console.log('[nocodb.getMembers] Fetching all members...'); const startTime = Date.now(); try { const result = await $fetch>(createTableUrl(Table.Members), { headers: { "xc-token": getNocoDbConfiguration().token, }, params: { limit: 1000, }, }); console.log('[nocodb.getMembers] Successfully fetched members, count:', result.list?.length || 0); console.log('[nocodb.getMembers] Request duration:', Date.now() - startTime, 'ms'); // DIAGNOSTIC: Log raw member data structure to identify schema issues if (result.list && result.list.length > 0) { const sampleMember = result.list[0]; console.log('[nocodb.getMembers] DIAGNOSTIC - Raw member fields from NocoDB:', Object.keys(sampleMember)); console.log('[nocodb.getMembers] DIAGNOSTIC - Sample member data:', JSON.stringify(sampleMember, null, 2)); console.log('[nocodb.getMembers] DIAGNOSTIC - first_name value:', sampleMember.first_name); console.log('[nocodb.getMembers] DIAGNOSTIC - last_name value:', sampleMember.last_name); console.log('[nocodb.getMembers] DIAGNOSTIC - First Name value:', (sampleMember as any)['First Name']); console.log('[nocodb.getMembers] DIAGNOSTIC - Last Name value:', (sampleMember as any)['Last Name']); } return result; } catch (error: any) { console.error('[nocodb.getMembers] Error fetching members:', error); throw error; } }; export const getMemberById = async (id: string): Promise => { console.log('[nocodb.getMemberById] Fetching member ID:', id); const result = await $fetch(`${createTableUrl(Table.Members)}/${id}`, { headers: { "xc-token": getNocoDbConfiguration().token, }, }); console.log('[nocodb.getMemberById] Successfully retrieved member:', result.Id); return result; }; export const createMember = async (data: Partial): Promise => { console.log('[nocodb.createMember] Creating member with fields:', Object.keys(data)); // Create a clean data object that matches the member schema const cleanData: Record = {}; // Only include fields that are part of the member schema const allowedFields = [ "first_name", "last_name", "email", "phone", "current_year_dues_paid", "nationality", "date_of_birth", "membership_date_paid", "payment_due_date", "membership_status", "address", "member_since", "keycloak_id" ]; // Filter the data to only include allowed fields for (const field of allowedFields) { if (field in data) { cleanData[field] = (data as any)[field]; } } // Remove any computed or relation fields that shouldn't be sent delete cleanData.Id; delete cleanData.CreatedAt; delete cleanData.UpdatedAt; delete cleanData.FullName; delete cleanData.FormattedPhone; // Fix date formatting for PostgreSQL if (cleanData['date_of_birth']) { cleanData['date_of_birth'] = convertDateFormat(cleanData['date_of_birth']); } if (cleanData['membership_date_paid']) { cleanData['membership_date_paid'] = convertDateFormat(cleanData['membership_date_paid']); } if (cleanData['payment_due_date']) { cleanData['payment_due_date'] = convertDateFormat(cleanData['payment_due_date']); } console.log('[nocodb.createMember] Clean data fields:', Object.keys(cleanData)); const url = createTableUrl(Table.Members); try { const result = await $fetch(url, { method: "POST", headers: { "xc-token": getNocoDbConfiguration().token, }, body: cleanData, }); console.log('[nocodb.createMember] Created member with ID:', result.Id); return result; } catch (error) { console.error('[nocodb.createMember] Create failed:', error); console.error('[nocodb.createMember] Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } }; export const updateMember = async (id: string, data: Partial, retryCount = 0): Promise => { console.log('[nocodb.updateMember] Updating member:', id, 'Retry:', retryCount); console.log('[nocodb.updateMember] Data fields:', Object.keys(data)); // First, try to verify the record exists if (retryCount === 0) { try { console.log('[nocodb.updateMember] Verifying record exists...'); const existingRecord = await getMemberById(id); console.log('[nocodb.updateMember] Record exists with ID:', existingRecord.Id); } catch (verifyError: any) { console.error('[nocodb.updateMember] Failed to verify record:', verifyError); if (verifyError.statusCode === 404 || verifyError.status === 404) { console.error('[nocodb.updateMember] Record verification failed - record not found'); } } } // Create a clean data object const cleanData: Record = {}; // Only include fields that are part of the member schema const allowedFields = [ "first_name", "last_name", "email", "phone", "current_year_dues_paid", "nationality", "date_of_birth", "membership_date_paid", "payment_due_date", "membership_status", "address", "member_since", "keycloak_id" ]; // Filter the data to only include allowed fields for (const field of allowedFields) { if (field in data) { const value = (data as any)[field]; // Handle clearing fields - NocoDB requires null for clearing, not undefined if (value === undefined) { cleanData[field] = null; console.log(`[nocodb.updateMember] Converting undefined to null for field: ${field}`); } else { cleanData[field] = value; } } } // Fix date formatting for PostgreSQL if (cleanData['date_of_birth']) { cleanData['date_of_birth'] = convertDateFormat(cleanData['date_of_birth']); } if (cleanData['membership_date_paid']) { cleanData['membership_date_paid'] = convertDateFormat(cleanData['membership_date_paid']); } if (cleanData['payment_due_date']) { cleanData['payment_due_date'] = convertDateFormat(cleanData['payment_due_date']); } console.log('[nocodb.updateMember] Clean data fields:', Object.keys(cleanData)); // PATCH requires ID in the body (not in URL) cleanData.Id = parseInt(id); const url = createTableUrl(Table.Members); try { console.log('[nocodb.updateMember] Sending PATCH request'); const result = await $fetch(url, { method: "PATCH", headers: { "xc-token": getNocoDbConfiguration().token, "Content-Type": "application/json" }, body: cleanData }); console.log('[nocodb.updateMember] Update successful for ID:', id); return result; } catch (error: any) { console.error('[nocodb.updateMember] Update failed:', error); console.error('[nocodb.updateMember] Error details:', error instanceof Error ? error.message : 'Unknown error'); // If it's a 404 error and we haven't retried too many times, wait and retry if ((error.statusCode === 404 || error.status === 404) && retryCount < 3) { console.error('[nocodb.updateMember] 404 Error - Record not found. This might be a sync delay.'); console.error(`Retrying in ${(retryCount + 1) * 1000}ms... (Attempt ${retryCount + 1}/3)`); // Wait with exponential backoff await new Promise(resolve => setTimeout(resolve, (retryCount + 1) * 1000)); // Retry the update return updateMember(id, data, retryCount + 1); } throw error; } }; export const deleteMember = async (id: string) => { const startTime = Date.now(); console.log('[nocodb.deleteMember] ========================='); console.log('[nocodb.deleteMember] DELETE operation started at:', new Date().toISOString()); console.log('[nocodb.deleteMember] Target ID:', id); const url = createTableUrl(Table.Members); console.log('[nocodb.deleteMember] URL:', url); const requestBody = { "Id": parseInt(id) }; console.log('[nocodb.deleteMember] Request configuration:'); console.log(' Method: DELETE'); console.log(' URL:', url); console.log(' Body:', JSON.stringify(requestBody, null, 2)); try { const result = await $fetch(url, { method: "DELETE", headers: { "xc-token": getNocoDbConfiguration().token, "Content-Type": "application/json" }, body: requestBody }); console.log('[nocodb.deleteMember] DELETE successful'); console.log('[nocodb.deleteMember] Duration:', Date.now() - startTime, 'ms'); return result; } catch (error: any) { console.error('[nocodb.deleteMember] DELETE FAILED'); console.error('[nocodb.deleteMember] Error type:', error.constructor.name); console.error('[nocodb.deleteMember] Error message:', error.message); console.error('[nocodb.deleteMember] Duration:', Date.now() - startTime, 'ms'); throw error; } }; // Centralized error handling export const handleNocoDbError = (error: any, operation: string, entityType: string = 'Member') => { console.error(`[nocodb.${operation}] =========================`); console.error(`[nocodb.${operation}] ERROR in ${operation} for ${entityType}`); console.error(`[nocodb.${operation}] Error type:`, error.constructor?.name || 'Unknown'); console.error(`[nocodb.${operation}] Error status:`, error.statusCode || error.status || 'Unknown'); console.error(`[nocodb.${operation}] Error message:`, error.message || 'Unknown error'); console.error(`[nocodb.${operation}] Error data:`, error.data); console.error(`[nocodb.${operation}] =========================`); // Provide more specific error messages if (error.statusCode === 401 || error.status === 401) { throw createError({ statusCode: 401, statusMessage: `Authentication failed when accessing ${entityType}. Please check your access permissions.` }); } else if (error.statusCode === 403 || error.status === 403) { throw createError({ statusCode: 403, statusMessage: `Access denied to ${entityType}. This feature requires appropriate privileges.` }); } else if (error.statusCode === 404 || error.status === 404) { throw createError({ statusCode: 404, statusMessage: `${entityType} not found. Please verify the record exists.` }); } else if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT') { throw createError({ statusCode: 503, statusMessage: `${entityType} database is temporarily unavailable. Please try again in a moment.` }); } throw error; };