import type { Interest, Berth } from "@/utils/types"; export interface PageInfo { pageSize: number; totalRows: number; isFirstPage: boolean; isLastPage: boolean; page: number; } export interface InterestsResponse { list: Interest[]; PageInfo: PageInfo; } export interface BerthsResponse { list: Berth[]; PageInfo: PageInfo; } export enum Table { Interest = "mbs9hjauug4eseo", Berth = "mczgos9hr3oa9qc", } /** * 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; }; export const getNocoDbConfiguration = () => { const config = useRuntimeConfig().nocodb; console.log('[nocodb] Configuration URL:', config.url); return config; }; export const createTableUrl = (table: Table) => { const url = `${getNocoDbConfiguration().url}/api/v2/tables/${table}/records`; console.log('[nocodb] Table URL:', url); return url; }; export const getInterests = async () => $fetch(createTableUrl(Table.Interest), { headers: { "xc-token": getNocoDbConfiguration().token, }, params: { limit: 1000, }, }); export const getInterestById = async (id: string) => { console.log('[nocodb.getInterestById] Fetching interest ID:', id); const result = await $fetch(`${createTableUrl(Table.Interest)}/${id}`, { headers: { "xc-token": getNocoDbConfiguration().token, }, }); console.log('[nocodb.getInterestById] Raw result from NocoDB:', { id: result.Id, documensoID: result['documensoID'], documensoID_type: typeof result['documensoID'], documensoID_value: JSON.stringify(result['documensoID']), signatureLinks: { client: result['Signature Link Client'], cc: result['Signature Link CC'], developer: result['Signature Link Developer'] } }); return result; }; export const updateInterest = async (id: string, data: Partial, retryCount = 0): Promise => { console.log('[nocodb.updateInterest] Updating interest:', id, 'Retry:', retryCount); console.log('[nocodb.updateInterest] Data fields:', Object.keys(data)); // First, try to verify the record exists if (retryCount === 0) { try { console.log('[nocodb.updateInterest] Verifying record exists...'); const existingRecord = await getInterestById(id); console.log('[nocodb.updateInterest] Record exists with ID:', existingRecord.Id); } catch (verifyError: any) { console.error('[nocodb.updateInterest] Failed to verify record:', verifyError); if (verifyError.statusCode === 404 || verifyError.status === 404) { console.error('[nocodb.updateInterest] Record verification failed - record not found'); } } } // Create a clean data object that matches the InterestsRequest schema // Remove any properties that are not in the schema or shouldn't be sent const cleanData: Record = {}; // Only include fields that are part of the InterestsRequest schema // Removed webhook fields: "Request More Information", "Request More Info - To Sales", "EOI Send to Sales" const allowedFields = [ "Full Name", "Yacht Name", "Length", "Address", "Email Address", "Sales Process Level", "Phone Number", "Extra Comments", "Berth Size Desired", "LOI-NDA Document", "Date Added", "Width", "Depth", "Created At", "Source", "Contact Method Preferred", "Request Form Sent", "Berth Number", "EOI Time Sent", "Lead Category", "Time LOI Sent", "EOI Status", "Berth Info Sent Status", "Contract Sent Status", "Deposit 10% Status", "Contract Status", // Add the EOI link fields "EOI Client Link", "EOI David Link", "EOI Oscar Link", "EOI Document", // Add the new signature link fields "Signature Link Client", "Signature Link CC", "Signature Link Developer", // Add the embedded signature link fields "EmbeddedSignatureLinkClient", "EmbeddedSignatureLinkCC", "EmbeddedSignatureLinkDeveloper", // Add the Documenso document ID field "documensoID" ]; // Filter the data to only include allowed fields for (const field of allowedFields) { if (field in data) { const value = (data as any)[field]; // Skip webhook-type fields and other object fields that shouldn't be sent if (value && typeof value === 'object' && !Array.isArray(value)) { console.log(`[nocodb.updateInterest] Skipping object field: ${field}`, value); continue; } // Handle clearing fields - NocoDB requires null for clearing, not undefined if (value === undefined) { cleanData[field] = null; console.log(`[nocodb.updateInterest] Converting undefined to null for field: ${field}`); } else { cleanData[field] = value; } } } // Fix date formatting for PostgreSQL if (cleanData['Date Added']) { cleanData['Date Added'] = convertDateFormat(cleanData['Date Added']); } if (cleanData['Created At']) { cleanData['Created At'] = convertDateFormat(cleanData['Created At']); } if (cleanData['EOI Time Sent']) { cleanData['EOI Time Sent'] = convertDateFormat(cleanData['EOI Time Sent']); } if (cleanData['Time LOI Sent']) { cleanData['Time LOI Sent'] = convertDateFormat(cleanData['Time LOI Sent']); } console.log('[nocodb.updateInterest] Clean data fields:', Object.keys(cleanData)); // PATCH requires ID in the body (not in URL) // Ensure ID is an integer cleanData.Id = parseInt(id); const url = createTableUrl(Table.Interest); console.log('[nocodb.updateInterest] URL:', url); try { console.log('[nocodb.updateInterest] Sending PATCH request with headers:', { "xc-token": getNocoDbConfiguration().token ? "***" + getNocoDbConfiguration().token.slice(-4) : "not set" }); console.log('[nocodb.updateInterest] Request body:', JSON.stringify(cleanData, null, 2)); // Try sending as a single object first (as shown in the API docs) const result = await $fetch(url, { method: "PATCH", headers: { "xc-token": getNocoDbConfiguration().token, "Content-Type": "application/json" }, body: cleanData }); console.log('[nocodb.updateInterest] Update successful for ID:', id); return result; } catch (error: any) { console.error('[nocodb.updateInterest] Update failed:', error); console.error('[nocodb.updateInterest] 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.updateInterest] 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 updateInterest(id, data, retryCount + 1); } // If it's still a 404 after retries, provide detailed error if (error.statusCode === 404 || error.status === 404) { console.error('[nocodb.updateInterest] 404 Error - Record not found after 3 retries. This might happen if:'); console.error('1. The record ID is incorrect'); console.error('2. The record was deleted'); console.error('3. There is a synchronization issue with the database'); console.error('Attempted URL:', url); } throw error; } }; export const createInterest = async (data: Partial) => { console.log('[nocodb.createInterest] Creating interest with fields:', Object.keys(data)); // Create a clean data object that matches the InterestsRequest schema const cleanData: Record = {}; // Only include fields that are part of the InterestsRequest schema const allowedFields = [ "Full Name", "Yacht Name", "Length", "Address", "Email Address", "Sales Process Level", "Phone Number", "Extra Comments", "Berth Size Desired", "Date Added", "Width", "Depth", "Source", "Contact Method Preferred", "Lead Category", "EOI Status", "Berth Info Sent Status", "Contract Sent Status", "Deposit 10% Status", "Contract Status", ]; // 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.Berths; delete cleanData["Berth Recommendations"]; delete cleanData.Berth; // Fix date formatting for PostgreSQL if (cleanData['Date Added']) { cleanData['Date Added'] = convertDateFormat(cleanData['Date Added']); } if (cleanData['Created At']) { cleanData['Created At'] = convertDateFormat(cleanData['Created At']); } console.log('[nocodb.createInterest] Clean data fields:', Object.keys(cleanData)); const url = createTableUrl(Table.Interest); console.log('[nocodb.createInterest] URL:', url); try { const result = await $fetch(url, { method: "POST", headers: { "xc-token": getNocoDbConfiguration().token, }, body: cleanData, }); console.log('[nocodb.createInterest] Created interest with ID:', result.Id); return result; } catch (error) { console.error('[nocodb.createInterest] Create failed:', error); console.error('[nocodb.createInterest] Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } }; export const deleteInterest = async (id: string) => { const startTime = Date.now(); console.log('[nocodb.deleteInterest] ========================='); console.log('[nocodb.deleteInterest] DELETE operation started at:', new Date().toISOString()); console.log('[nocodb.deleteInterest] Target ID:', id); const url = createTableUrl(Table.Interest); console.log('[nocodb.deleteInterest] URL:', url); const requestBody = { "Id": parseInt(id) }; console.log('[nocodb.deleteInterest] Request configuration:'); console.log(' Method: DELETE'); console.log(' URL:', url); console.log(' Headers:', { "xc-token": getNocoDbConfiguration().token ? "***" + getNocoDbConfiguration().token.slice(-4) : "not set", "Content-Type": "application/json" }); console.log(' Body:', JSON.stringify(requestBody, null, 2)); try { // According to NocoDB API docs, DELETE requires ID in the body const result = await $fetch(url, { method: "DELETE", headers: { "xc-token": getNocoDbConfiguration().token, "Content-Type": "application/json" }, body: requestBody }); console.log('[nocodb.deleteInterest] DELETE successful'); console.log('[nocodb.deleteInterest] Response:', JSON.stringify(result, null, 2)); console.log('[nocodb.deleteInterest] Duration:', Date.now() - startTime, 'ms'); console.log('[nocodb.deleteInterest] ========================='); return result; } catch (error: any) { console.error('[nocodb.deleteInterest] ========================='); console.error('[nocodb.deleteInterest] DELETE FAILED'); console.error('[nocodb.deleteInterest] Error type:', error.constructor.name); console.error('[nocodb.deleteInterest] Error message:', error.message); console.error('[nocodb.deleteInterest] Error status:', error.statusCode || error.status || 'unknown'); console.error('[nocodb.deleteInterest] Error data:', error.data); console.error('[nocodb.deleteInterest] Error stack:', error.stack || 'No stack trace'); console.error('[nocodb.deleteInterest] Full error:', JSON.stringify(error, null, 2)); console.error('[nocodb.deleteInterest] Duration:', Date.now() - startTime, 'ms'); console.error('[nocodb.deleteInterest] ========================='); throw error; } }; export const triggerWebhook = async (url: string, payload: any) => $fetch(url, { method: "POST", body: payload, }); export const updateInterestEOIDocument = async (id: string, documentData: any) => { console.log('[nocodb.updateInterestEOIDocument] Updating EOI document for interest:', id); // Get existing EOI Document array or create new one const interest = await getInterestById(id); const existingDocuments = interest['EOI Document'] || []; // Add the new document to the array const updatedDocuments = [...existingDocuments, documentData]; // Update the interest with the new EOI Document array return updateInterest(id, { 'EOI Document': updatedDocuments }); }; export const getInterestByFieldAsync = async (fieldName: string, value: any): Promise => { try { const response = await getInterests(); const interests = response.list || []; // Find interest where the field matches the value const interest = interests.find(i => (i as any)[fieldName] === value); return interest || null; } catch (error) { console.error('Error fetching interest by field:', error); return null; } }; // Berth functions export const getBerths = async () => { console.log('[nocodb.getBerths] Fetching berths from NocoDB...'); try { // First try with basic query - no field expansion to avoid 404 const result = await $fetch(createTableUrl(Table.Berth), { headers: { "xc-token": getNocoDbConfiguration().token, }, params: { limit: 1000, // Start with basic fields only fields: '*' }, }); console.log('[nocodb.getBerths] Successfully fetched berths, count:', result.list?.length || 0); // Process each berth to populate interested parties details if (result.list && Array.isArray(result.list)) { console.log('[nocodb.getBerths] Processing berths to populate interested parties...'); await Promise.all( result.list.map(async (berth) => { if (berth['Interested Parties'] && Array.isArray(berth['Interested Parties'])) { const interestedPartiesDetails = await Promise.all( berth['Interested Parties'].map(async (party: any) => { if (party && (party.Id || party.id)) { const interestId = party.Id || party.id; try { const interestDetails = await getInterestById(interestId.toString()); return interestDetails; } catch (error) { console.error('[nocodb.getBerths] Failed to fetch interest details for ID:', interestId, error); return party; // Return original party if fetch fails } } return party; }) ); berth['Interested Parties'] = interestedPartiesDetails; } }) ); // Sort berths by letter zone and then by number using Mooring Number result.list.sort((a, b) => { const berthA = a['Mooring Number'] || ''; const berthB = b['Mooring Number'] || ''; // Extract letter and number parts const matchA = berthA.match(/^([A-Za-z]+)(\d+)$/); const matchB = berthB.match(/^([A-Za-z]+)(\d+)$/); if (matchA && matchB) { const [, letterA, numberA] = matchA; const [, letterB, numberB] = matchB; // First sort by letter zone const letterCompare = letterA.localeCompare(letterB); if (letterCompare !== 0) { return letterCompare; } // Then sort by number within the same letter zone return parseInt(numberA) - parseInt(numberB); } // Fallback to string comparison if pattern doesn't match return berthA.localeCompare(berthB); }); console.log('[nocodb.getBerths] Berths sorted by zone and number with populated interested parties'); } return result; } catch (error: any) { console.error('[nocodb.getBerths] Error fetching berths:', error); console.error('[nocodb.getBerths] Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } }; export const getBerthById = async (id: string) => { console.log('[nocodb.getBerthById] Fetching berth ID:', id); try { // First fetch the basic berth data const result = await $fetch(`${createTableUrl(Table.Berth)}/${id}`, { headers: { "xc-token": getNocoDbConfiguration().token, }, params: { fields: '*' } }); console.log('[nocodb.getBerthById] Successfully fetched berth:', result.Id); // Now fetch and populate the interested parties details if (result['Interested Parties'] && Array.isArray(result['Interested Parties'])) { console.log('[nocodb.getBerthById] Fetching details for interested parties:', result['Interested Parties'].length); const interestedPartiesDetails = await Promise.all( result['Interested Parties'].map(async (party: any) => { if (party && (party.Id || party.id)) { const interestId = party.Id || party.id; try { console.log('[nocodb.getBerthById] Fetching interest details for ID:', interestId); const interestDetails = await getInterestById(interestId.toString()); return interestDetails; } catch (error) { console.error('[nocodb.getBerthById] Failed to fetch interest details for ID:', interestId, error); return party; // Return original party if fetch fails } } return party; }) ); result['Interested Parties'] = interestedPartiesDetails; console.log('[nocodb.getBerthById] Populated interested parties details'); } return result; } catch (error: any) { console.error('[nocodb.getBerthById] Error fetching berth:', error); console.error('[nocodb.getBerthById] Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } }; export const updateBerth = async (id: string, data: Partial): Promise => { console.log('[nocodb.updateBerth] Updating berth:', id); console.log('[nocodb.updateBerth] Data fields:', Object.keys(data)); // Create a clean data object that matches the Berth schema const cleanData: Record = {}; // Only include fields that are part of the Berth schema (excluding formula fields) const allowedFields = [ "Mooring Number", "Area", "Status", "Nominal Boat Size", "Water Depth", "Length", "Width", "Draft", // Changed from "Depth" to "Draft" "Side Pontoon", "Power Capacity", "Voltage", "Mooring Type", "Access", "Cleat Type", "Cleat Capacity", "Bollard Type", "Bollard Capacity", "Price", "Bow Facing" ]; // 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.updateBerth] Converting undefined to null for field: ${field}`); } else { cleanData[field] = value; } } } console.log('[nocodb.updateBerth] Clean data fields:', Object.keys(cleanData)); // PATCH requires ID in the body (not in URL) // Ensure ID is an integer cleanData.Id = parseInt(id); const url = createTableUrl(Table.Berth); console.log('[nocodb.updateBerth] URL:', url); try { console.log('[nocodb.updateBerth] Sending PATCH request'); const result = await $fetch(url, { method: "PATCH", headers: { "xc-token": getNocoDbConfiguration().token, "Content-Type": "application/json" }, body: cleanData }); console.log('[nocodb.updateBerth] Update successful for ID:', id); return result; } catch (error: any) { console.error('[nocodb.updateBerth] Update failed:', error); console.error('[nocodb.updateBerth] Error details:', error instanceof Error ? error.message : 'Unknown error'); throw error; } };