port-nimara-client-portal/server/utils/nocodb.ts

688 lines
24 KiB
TypeScript
Raw Normal View History

import type { Interest, Berth } from "@/utils/types";
2025-05-29 07:32:13 +02:00
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;
}
2025-05-29 07:32:13 +02:00
export enum Table {
Interest = "mbs9hjauug4eseo",
Berth = "mczgos9hr3oa9qc",
2025-05-29 07:32:13 +02:00
}
/**
* 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;
};
2025-06-09 23:42:31 +02:00
export const getNocoDbConfiguration = () => {
const config = useRuntimeConfig().nocodb;
console.log('[nocodb] Configuration URL:', config.url);
return config;
};
2025-05-29 07:32:13 +02:00
2025-06-09 23:42:31 +02:00
export const createTableUrl = (table: Table) => {
const url = `${getNocoDbConfiguration().url}/api/v2/tables/${table}/records`;
console.log('[nocodb] Table URL:', url);
return url;
};
2025-05-29 07:32:13 +02:00
2025-06-12 21:54:47 +02:00
export const getInterests = async () =>
$fetch<InterestsResponse>(createTableUrl(Table.Interest), {
headers: {
"xc-token": getNocoDbConfiguration().token,
2025-05-29 07:32:13 +02:00
},
2025-06-12 21:54:47 +02:00
params: {
limit: 1000,
},
});
2025-05-29 07:32:13 +02:00
2025-06-11 16:05:19 +02:00
export const getInterestById = async (id: string) => {
console.log('[nocodb.getInterestById] Fetching interest ID:', id);
2025-06-12 21:54:47 +02:00
const result = await $fetch<Interest>(`${createTableUrl(Table.Interest)}/${id}`, {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
});
2025-06-11 16:05:19 +02:00
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;
};
2025-06-03 17:57:08 +02:00
2025-06-09 23:38:35 +02:00
export const updateInterest = async (id: string, data: Partial<Interest>, retryCount = 0): Promise<Interest> => {
console.log('[nocodb.updateInterest] Updating interest:', id, 'Retry:', retryCount);
console.log('[nocodb.updateInterest] Data fields:', Object.keys(data));
2025-06-09 23:42:31 +02:00
// 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');
}
}
}
2025-06-03 17:57:08 +02:00
// 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<string, any> = {};
2025-06-03 21:04:22 +02:00
2025-06-03 17:57:08 +02:00
// Only include fields that are part of the InterestsRequest schema
2025-06-09 23:48:00 +02:00
// Removed webhook fields: "Request More Information", "Request More Info - To Sales", "EOI Send to Sales"
2025-06-03 17:57:08 +02:00
const allowedFields = [
2025-06-03 21:04:22 +02:00
"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",
2025-06-03 23:48:44 +02:00
"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",
2025-06-11 14:28:03 +02:00
"Signature Link Developer",
2025-06-11 18:59:16 +02:00
// Add the embedded signature link fields
"EmbeddedSignatureLinkClient",
"EmbeddedSignatureLinkCC",
"EmbeddedSignatureLinkDeveloper",
2025-06-11 14:28:03 +02:00
// Add the Documenso document ID field
"documensoID"
2025-06-03 17:57:08 +02:00
];
2025-06-03 21:04:22 +02:00
2025-06-03 17:57:08 +02:00
// Filter the data to only include allowed fields
for (const field of allowedFields) {
if (field in data) {
2025-06-09 23:48:00 +02:00
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;
}
2025-06-03 17:57:08 +02:00
}
}
2025-06-03 21:04:22 +02:00
// 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));
2025-06-10 00:15:36 +02:00
// 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 {
2025-06-09 23:42:31 +02:00
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));
2025-06-10 00:15:36 +02:00
// Try sending as a single object first (as shown in the API docs)
2025-06-12 21:54:47 +02:00
const result = await $fetch<Interest>(url, {
method: "PATCH",
2025-06-12 21:54:47 +02:00
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');
2025-06-09 23:38:35 +02:00
// 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) {
2025-06-09 23:38:35 +02:00
console.error('[nocodb.updateInterest] 404 Error - Record not found after 3 retries. This might happen if:');
console.error('1. The record ID is incorrect');
2025-06-09 23:38:35 +02:00
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;
}
2025-06-03 21:04:22 +02:00
};
2025-06-12 21:54:47 +02:00
export const createInterest = async (data: Partial<Interest>) => {
console.log('[nocodb.createInterest] Creating interest with fields:', Object.keys(data));
2025-06-03 21:04:22 +02:00
// Create a clean data object that matches the InterestsRequest schema
const cleanData: Record<string, any> = {};
// 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",
2025-06-03 23:48:44 +02:00
"EOI Status",
"Berth Info Sent Status",
"Contract Sent Status",
"Deposit 10% Status",
"Contract Status",
2025-06-03 21:04:22 +02:00
];
// Filter the data to only include allowed fields
for (const field of allowedFields) {
if (field in data) {
cleanData[field] = (data as any)[field];
2025-06-03 21:04:22 +02:00
}
}
// 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 {
2025-06-12 21:54:47 +02:00
const result = await $fetch<Interest>(url, {
method: "POST",
2025-06-12 21:54:47 +02:00
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;
}
2025-06-03 17:57:08 +02:00
};
export const deleteInterest = async (id: string) => {
2025-06-10 12:54:22 +02:00
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);
2025-06-10 12:31:00 +02:00
const url = createTableUrl(Table.Interest);
console.log('[nocodb.deleteInterest] URL:', url);
2025-06-10 12:54:22 +02:00
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 {
2025-06-10 12:31:00 +02:00
// According to NocoDB API docs, DELETE requires ID in the body
2025-06-12 21:54:47 +02:00
const result = await $fetch(url, {
method: "DELETE",
2025-06-12 21:54:47 +02:00
headers: {
"xc-token": getNocoDbConfiguration().token,
"Content-Type": "application/json"
},
body: requestBody
});
2025-06-10 12:54:22 +02:00
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;
2025-06-10 12:54:22 +02:00
} 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;
}
};
2025-06-12 21:54:47 +02:00
export const triggerWebhook = async (url: string, payload: any) =>
$fetch(url, {
2025-06-03 21:04:22 +02:00
method: "POST",
2025-06-12 21:54:47 +02:00
body: payload,
2025-06-03 17:57:08 +02:00
});
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<Interest | null> => {
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<BerthsResponse>(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...');
// Count berths with interested parties
const berthsWithParties = result.list.filter(b =>
b['Interested Parties'] && Array.isArray(b['Interested Parties']) && b['Interested Parties'].length > 0
);
console.log('[nocodb.getBerths] Berths with interested parties:', berthsWithParties.length);
// Log first berth with interested parties for debugging
if (berthsWithParties.length > 0) {
const firstBerth = berthsWithParties[0];
if (firstBerth && firstBerth['Interested Parties']) {
console.log('[nocodb.getBerths] First berth with parties:', {
id: firstBerth.Id,
mooringNumber: firstBerth['Mooring Number'],
partiesCount: firstBerth['Interested Parties'].length,
firstParty: firstBerth['Interested Parties'][0]
});
}
}
await Promise.all(
result.list.map(async (berth) => {
if (berth['Interested Parties'] && Array.isArray(berth['Interested Parties']) && berth['Interested Parties'].length > 0) {
console.log(`[nocodb.getBerths] Processing ${berth['Interested Parties'].length} parties for berth ${berth['Mooring Number']}`);
// Extract IDs from various possible formats
const partyIds = berth['Interested Parties'].map((party: any) => {
// Handle different possible formats from NocoDB
if (typeof party === 'number') return party;
if (typeof party === 'string') return parseInt(party);
if (party && typeof party === 'object') {
// Check various possible ID field names
return party.Id || party.id || party.ID || party._id || null;
}
return null;
}).filter((id: any) => id !== null && !isNaN(id));
console.log(`[nocodb.getBerths] Extracted ${partyIds.length} valid IDs for berth ${berth['Mooring Number']}:`, partyIds);
if (partyIds.length > 0) {
const interestedPartiesDetails = await Promise.all(
partyIds.map(async (partyId: number) => {
try {
console.log(`[nocodb.getBerths] Fetching interest ${partyId} for berth ${berth['Mooring Number']}`);
const interestDetails = await getInterestById(partyId.toString());
return interestDetails;
} catch (error) {
console.error(`[nocodb.getBerths] Failed to fetch interest ${partyId}:`, error);
return { Id: partyId, 'Full Name': `Interest #${partyId}` } as any;
}
})
);
berth['Interested Parties'] = interestedPartiesDetails;
console.log(`[nocodb.getBerths] Populated ${interestedPartiesDetails.length} parties for berth ${berth['Mooring Number']}`);
} else {
console.log(`[nocodb.getBerths] No valid party IDs found for berth ${berth['Mooring Number']}`);
}
}
})
);
// 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<Berth>(`${createTableUrl(Table.Berth)}/${id}`, {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
params: {
fields: '*'
}
});
console.log('[nocodb.getBerthById] Successfully fetched berth:', result.Id);
console.log('[nocodb.getBerthById] Raw Interested Parties:', JSON.stringify(result['Interested Parties'], null, 2));
// 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);
// Extract IDs from various possible formats
const partyIds = result['Interested Parties'].map((party: any) => {
// Handle different possible formats from NocoDB
if (typeof party === 'number') return party;
if (typeof party === 'string') return parseInt(party);
if (party && typeof party === 'object') {
// Check various possible ID field names
return party.Id || party.id || party.ID || party._id || null;
}
return null;
}).filter(id => id !== null && !isNaN(id));
console.log('[nocodb.getBerthById] Extracted party IDs:', partyIds);
// Fetch full interest records
if (partyIds.length > 0) {
const interestedPartiesDetails = await Promise.all(
partyIds.map(async (partyId: number) => {
try {
console.log('[nocodb.getBerthById] Fetching interest details for ID:', partyId);
const interestDetails = await getInterestById(partyId.toString());
return interestDetails;
} catch (error) {
console.error('[nocodb.getBerthById] Failed to fetch interest details for ID:', partyId, error);
// Return a placeholder object if fetch fails
return { Id: partyId, 'Full Name': `Interest #${partyId}` } as any;
}
})
);
result['Interested Parties'] = interestedPartiesDetails;
console.log('[nocodb.getBerthById] Populated interested parties details:', interestedPartiesDetails.length);
} else {
console.log('[nocodb.getBerthById] No valid party IDs found to populate');
}
}
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<Berth>): Promise<Berth> => {
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<string, any> = {};
// 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<Berth>(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;
}
};