2025-07-03 21:29:42 +02:00
import type { Interest , Berth , Expense , ExpenseFilters } from "@/utils/types" ;
2025-06-17 16:07:15 +02:00
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 ;
}
2025-06-17 16:07:15 +02:00
export interface BerthsResponse {
list : Berth [ ] ;
PageInfo : PageInfo ;
}
2025-07-03 21:29:42 +02:00
export interface ExpensesResponse {
list : Expense [ ] ;
PageInfo : PageInfo ;
}
2025-05-29 07:32:13 +02:00
export enum Table {
Interest = "mbs9hjauug4eseo" ,
2025-06-17 16:07:15 +02:00
Berth = "mczgos9hr3oa9qc" ,
2025-07-03 21:29:42 +02:00
Expense = "mxfcefkk4dqs6uq" , // Expense tracking table
2025-05-29 07:32:13 +02:00
}
2025-06-15 16:52:26 +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 ) ;
2025-06-09 23:19:52 +02:00
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" ,
2025-06-09 23:19:52 +02:00
// Add the EOI link fields
"EOI Client Link" ,
2025-06-10 00:37:43 +02:00
"EOI David Link" ,
2025-06-09 23:19:52 +02:00
"EOI Oscar Link" ,
2025-06-10 00:37:43 +02:00
"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 ;
}
2025-06-12 17:36:27 +02:00
// 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
2025-06-15 16:52:26 +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' ] ) ;
}
2025-06-09 23:19:52 +02:00
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 ) ;
2025-06-09 23:19:52 +02:00
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 , {
2025-06-09 23:19:52 +02:00
method : "PATCH" ,
2025-06-12 21:54:47 +02:00
headers : {
"xc-token" : getNocoDbConfiguration ( ) . token ,
"Content-Type" : "application/json"
} ,
body : cleanData
2025-06-09 23:19:52 +02:00
} ) ;
console . log ( '[nocodb.updateInterest] Update successful for ID:' , id ) ;
return result ;
2025-06-09 23:29:24 +02:00
} catch ( error : any ) {
2025-06-09 23:19:52 +02:00
console . error ( '[nocodb.updateInterest] Update failed:' , error ) ;
console . error ( '[nocodb.updateInterest] Error details:' , error instanceof Error ? error . message : 'Unknown error' ) ;
2025-06-09 23:29:24 +02:00
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
2025-06-09 23:29:24 +02:00
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:' ) ;
2025-06-09 23:29:24 +02:00
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' ) ;
2025-06-09 23:29:24 +02:00
console . error ( 'Attempted URL:' , url ) ;
}
2025-06-09 23:19:52 +02:00
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 > ) = > {
2025-06-09 23:19:52 +02:00
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 ) {
2025-06-04 19:51:51 +02:00
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 ;
2025-06-15 16:52:26 +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' ] ) ;
}
2025-06-09 23:19:52 +02:00
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 , {
2025-06-09 23:19:52 +02:00
method : "POST" ,
2025-06-12 21:54:47 +02:00
headers : {
"xc-token" : getNocoDbConfiguration ( ) . token ,
} ,
body : cleanData ,
2025-06-09 23:19:52 +02:00
} ) ;
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
} ;
2025-06-09 23:19:52 +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 ) ;
2025-06-09 23:19:52 +02:00
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 ) ) ;
2025-06-09 23:19:52 +02:00
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 , {
2025-06-09 23:19:52 +02:00
method : "DELETE" ,
2025-06-12 21:54:47 +02:00
headers : {
"xc-token" : getNocoDbConfiguration ( ) . token ,
"Content-Type" : "application/json"
} ,
body : requestBody
2025-06-09 23:19:52 +02:00
} ) ;
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] =========================' ) ;
2025-06-09 23:19:52 +02:00
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] =========================' ) ;
2025-06-09 23:19:52 +02:00
throw error ;
}
} ;
2025-06-04 19:51:51 +02:00
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
} ) ;
2025-06-10 13:59:09 +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 ;
}
} ;
2025-06-17 16:07:15 +02:00
// Berth functions
export const getBerths = async ( ) = > {
console . log ( '[nocodb.getBerths] Fetching berths from NocoDB...' ) ;
2025-06-17 16:18:29 +02:00
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 : '*'
} ,
} ) ;
2025-06-17 16:07:15 +02:00
2025-06-17 16:18:29 +02:00
console . log ( '[nocodb.getBerths] Successfully fetched berths, count:' , result . list ? . length || 0 ) ;
2025-06-17 16:27:32 +02:00
// Process each berth to populate interested parties details
2025-06-17 16:18:29 +02:00
if ( result . list && Array . isArray ( result . list ) ) {
2025-06-17 16:27:32 +02:00
console . log ( '[nocodb.getBerths] Processing berths to populate interested parties...' ) ;
2025-06-17 17:24:04 +02:00
// Count berths with interested parties (NocoDB returns count as number)
2025-06-17 17:04:45 +02:00
const berthsWithParties = result . list . filter ( b = >
2025-06-17 17:24:04 +02:00
b [ 'Interested Parties' ] && ( typeof b [ 'Interested Parties' ] === 'number' && b [ 'Interested Parties' ] > 0 )
2025-06-17 17:04:45 +02:00
) ;
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 ]
} ) ;
}
}
2025-06-17 16:27:32 +02:00
await Promise . all (
result . list . map ( async ( berth ) = > {
2025-06-17 17:24:04 +02:00
// Check if berth has interested parties (as a number count)
if ( berth [ 'Interested Parties' ] && typeof berth [ 'Interested Parties' ] === 'number' && berth [ 'Interested Parties' ] > 0 ) {
const partyCount = berth [ 'Interested Parties' ] as number ;
console . log ( ` [nocodb.getBerths] Berth ${ berth [ 'Mooring Number' ] } has ${ partyCount } interested parties (as count) ` ) ;
// When we have a count, fetch the linked records using the links API
try {
const config = getNocoDbConfiguration ( ) ;
const berthsTableId = "mczgos9hr3oa9qc" ;
const interestedPartiesFieldId = "c7q2z2rb27c1cb5" ;
const linkUrl = ` ${ config . url } /api/v2/tables/ ${ berthsTableId } /links/ ${ interestedPartiesFieldId } /records/ ${ berth . Id } ` ;
console . log ( ` [nocodb.getBerths] Fetching linked parties from: ${ linkUrl } ` ) ;
const linkedResponse = await $fetch < any > ( linkUrl , {
headers : {
"xc-token" : config . token ,
} ,
params : {
limit : 100
}
} ) ;
console . log ( ` [nocodb.getBerths] Linked response for berth ${ berth [ 'Mooring Number' ] } : ` , linkedResponse ) ;
if ( linkedResponse && linkedResponse . list && Array . isArray ( linkedResponse . list ) ) {
2025-06-17 17:34:29 +02:00
// The links API returns limited data, so we need to fetch full records
console . log ( ` [nocodb.getBerths] Got ${ linkedResponse . list . length } linked records, fetching full details... ` ) ;
const fullInterestDetails = await Promise . all (
linkedResponse . list . map ( async ( linkedParty : any ) = > {
try {
const partyId = linkedParty . Id || linkedParty . id ;
if ( partyId ) {
const fullDetails = await getInterestById ( partyId . toString ( ) ) ;
return fullDetails ;
}
return linkedParty ;
} catch ( error ) {
console . error ( ` [nocodb.getBerths] Failed to fetch full details for party ${ linkedParty . Id } : ` , error ) ;
return linkedParty ;
}
} )
) ;
berth [ 'Interested Parties' ] = fullInterestDetails ;
console . log ( ` [nocodb.getBerths] Successfully fetched full details for ${ fullInterestDetails . length } interested parties for berth ${ berth [ 'Mooring Number' ] } ` ) ;
2025-06-17 17:24:04 +02:00
} else {
// Fallback to placeholders if API call doesn't return expected format
const placeholderParties = Array . from ( { length : partyCount } , ( _ , index ) = > ( {
Id : index + 1 ,
'Full Name' : ` Party ${ index + 1 } ` ,
'Sales Process Level' : null ,
'EOI Status' : null ,
'Contract Status' : null
} ) ) ;
berth [ 'Interested Parties' ] = placeholderParties as any ;
console . log ( ` [nocodb.getBerths] Using placeholders for berth ${ berth [ 'Mooring Number' ] } ` ) ;
}
} catch ( linkError ) {
console . error ( ` [nocodb.getBerths] Failed to fetch linked parties for berth ${ berth [ 'Mooring Number' ] } : ` , linkError ) ;
// Fallback to placeholders on error
const placeholderParties = Array . from ( { length : partyCount } , ( _ , index ) = > ( {
Id : index + 1 ,
'Full Name' : ` Party ${ index + 1 } ` ,
'Sales Process Level' : null ,
'EOI Status' : null ,
'Contract Status' : null
} ) ) ;
berth [ 'Interested Parties' ] = placeholderParties as any ;
}
} else if ( berth [ 'Interested Parties' ] && Array . isArray ( berth [ 'Interested Parties' ] ) && berth [ 'Interested Parties' ] . length > 0 ) {
// Handle case where we get an array (this might happen in some cases)
2025-06-17 17:04:45 +02:00
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 ) = > {
2025-06-17 16:27:32 +02:00
try {
2025-06-17 17:04:45 +02:00
console . log ( ` [nocodb.getBerths] Fetching interest ${ partyId } for berth ${ berth [ 'Mooring Number' ] } ` ) ;
const interestDetails = await getInterestById ( partyId . toString ( ) ) ;
2025-06-17 16:27:32 +02:00
return interestDetails ;
} catch ( error ) {
2025-06-17 17:04:45 +02:00
console . error ( ` [nocodb.getBerths] Failed to fetch interest ${ partyId } : ` , error ) ;
return { Id : partyId , 'Full Name' : ` Interest # ${ partyId } ` } as any ;
2025-06-17 16:27:32 +02:00
}
2025-06-17 17:04:45 +02:00
} )
) ;
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' ] } ` ) ;
}
2025-06-17 16:27:32 +02:00
}
} )
) ;
// Sort berths by letter zone and then by number using Mooring Number
2025-06-17 16:18:29 +02:00
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+)$/ ) ;
2025-06-17 16:07:15 +02:00
2025-06-17 16:18:29 +02:00
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 ) ;
2025-06-17 16:07:15 +02:00
}
2025-06-17 16:18:29 +02:00
// Fallback to string comparison if pattern doesn't match
return berthA . localeCompare ( berthB ) ;
} ) ;
2025-06-17 16:07:15 +02:00
2025-06-17 16:27:32 +02:00
console . log ( '[nocodb.getBerths] Berths sorted by zone and number with populated interested parties' ) ;
2025-06-17 16:18:29 +02:00
}
2025-06-17 16:07:15 +02:00
2025-06-17 16:18:29 +02:00
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 ;
2025-06-17 16:07:15 +02:00
}
} ;
export const getBerthById = async ( id : string ) = > {
console . log ( '[nocodb.getBerthById] Fetching berth ID:' , id ) ;
2025-06-17 16:18:29 +02:00
try {
2025-06-17 16:27:32 +02:00
// First fetch the basic berth data
2025-06-17 16:18:29 +02:00
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 ) ;
2025-06-17 16:49:43 +02:00
console . log ( '[nocodb.getBerthById] Raw Interested Parties:' , JSON . stringify ( result [ 'Interested Parties' ] , null , 2 ) ) ;
2025-06-17 16:27:32 +02:00
// Now fetch and populate the interested parties details
2025-06-17 17:24:04 +02:00
if ( result [ 'Interested Parties' ] ) {
// Handle case where interested parties is a count (number)
if ( typeof result [ 'Interested Parties' ] === 'number' && result [ 'Interested Parties' ] > 0 ) {
const partyCount = result [ 'Interested Parties' ] as number ;
console . log ( ` [nocodb.getBerthById] Berth has ${ partyCount } interested parties (as count) ` ) ;
// Fetch the linked records using the links API
try {
const config = getNocoDbConfiguration ( ) ;
const berthsTableId = "mczgos9hr3oa9qc" ;
const interestedPartiesFieldId = "c7q2z2rb27c1cb5" ;
const linkUrl = ` ${ config . url } /api/v2/tables/ ${ berthsTableId } /links/ ${ interestedPartiesFieldId } /records/ ${ result . Id } ` ;
console . log ( ` [nocodb.getBerthById] Fetching linked parties from: ${ linkUrl } ` ) ;
const linkedResponse = await $fetch < any > ( linkUrl , {
headers : {
"xc-token" : config . token ,
} ,
params : {
limit : 100
2025-06-17 16:27:32 +02:00
}
2025-06-17 17:24:04 +02:00
} ) ;
console . log ( ` [nocodb.getBerthById] Linked response: ` , linkedResponse ) ;
if ( linkedResponse && linkedResponse . list && Array . isArray ( linkedResponse . list ) ) {
2025-06-17 17:34:29 +02:00
// The links API returns limited data, so we need to fetch full records
console . log ( ` [nocodb.getBerthById] Got ${ linkedResponse . list . length } linked records, fetching full details... ` ) ;
const fullInterestDetails = await Promise . all (
linkedResponse . list . map ( async ( linkedParty : any ) = > {
try {
const partyId = linkedParty . Id || linkedParty . id ;
if ( partyId ) {
const fullDetails = await getInterestById ( partyId . toString ( ) ) ;
return fullDetails ;
}
return linkedParty ;
} catch ( error ) {
console . error ( ` [nocodb.getBerthById] Failed to fetch full details for party ${ linkedParty . Id } : ` , error ) ;
return linkedParty ;
}
} )
) ;
result [ 'Interested Parties' ] = fullInterestDetails ;
console . log ( ` [nocodb.getBerthById] Successfully fetched full details for ${ fullInterestDetails . length } interested parties ` ) ;
2025-06-17 17:24:04 +02:00
} else {
// Fallback to placeholders if API call doesn't return expected format
const placeholderParties = Array . from ( { length : partyCount } , ( _ , index ) = > ( {
Id : index + 1 ,
'Full Name' : ` Party ${ index + 1 } ` ,
'Sales Process Level' : null ,
'EOI Status' : null ,
'Contract Status' : null
} ) ) ;
result [ 'Interested Parties' ] = placeholderParties as any ;
console . log ( ` [nocodb.getBerthById] Using placeholders ` ) ;
}
} catch ( linkError ) {
console . error ( ` [nocodb.getBerthById] Failed to fetch linked parties: ` , linkError ) ;
// Fallback to placeholders on error
const placeholderParties = Array . from ( { length : partyCount } , ( _ , index ) = > ( {
Id : index + 1 ,
'Full Name' : ` Party ${ index + 1 } ` ,
'Sales Process Level' : null ,
'EOI Status' : null ,
'Contract Status' : null
} ) ) ;
result [ 'Interested Parties' ] = placeholderParties as any ;
}
} else if ( Array . isArray ( result [ 'Interested Parties' ] ) ) {
// Handle case where we get an array (might happen in some cases)
console . log ( '[nocodb.getBerthById] Fetching details for interested parties:' , result [ 'Interested Parties' ] . length ) ;
2025-06-17 16:49:43 +02:00
2025-06-17 17:24:04 +02:00
// 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' ) ;
}
2025-06-17 16:49:43 +02:00
}
2025-06-17 16:27:32 +02:00
}
2025-06-17 16:18:29 +02:00
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 ;
}
2025-06-17 16:07:15 +02:00
} ;
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 > = { } ;
2025-06-17 16:18:29 +02:00
// Only include fields that are part of the Berth schema (excluding formula fields)
2025-06-17 16:07:15 +02:00
const allowedFields = [
"Mooring Number" ,
"Area" ,
"Status" ,
"Nominal Boat Size" ,
"Water Depth" ,
"Length" ,
"Width" ,
2025-06-17 16:18:29 +02:00
"Draft" , // Changed from "Depth" to "Draft"
2025-06-17 16:07:15 +02:00
"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 ;
}
} ;
2025-07-03 21:29:42 +02:00
// Expense functions
export const getExpenses = async ( filters? : ExpenseFilters ) = > {
console . log ( '[nocodb.getExpenses] Fetching expenses from NocoDB...' , filters ) ;
try {
const params : any = { limit : 1000 } ;
// Build filter conditions
if ( filters ? . startDate && filters ? . endDate ) {
params . where = ` (Time,gte, ${ filters . startDate } )~and(Time,lte, ${ filters . endDate } ) ` ;
} else if ( filters ? . startDate ) {
params . where = ` (Time,gte, ${ filters . startDate } ) ` ;
} else if ( filters ? . endDate ) {
params . where = ` (Time,lte, ${ filters . endDate } ) ` ;
}
// Add payer filter
if ( filters ? . payer ) {
const payerFilter = ` (Payer,eq, ${ filters . payer } ) ` ;
params . where = params . where ? ` ${ params . where } ~and ${ payerFilter } ` : payerFilter ;
}
// Add category filter
if ( filters ? . category ) {
const categoryFilter = ` (Category,eq, ${ filters . category } ) ` ;
params . where = params . where ? ` ${ params . where } ~and ${ categoryFilter } ` : categoryFilter ;
}
// Sort by Time descending (newest first)
params . sort = '-Time' ;
console . log ( '[nocodb.getExpenses] Request params:' , params ) ;
const result = await $fetch < ExpensesResponse > ( createTableUrl ( Table . Expense ) , {
headers : {
"xc-token" : getNocoDbConfiguration ( ) . token ,
} ,
params
} ) ;
console . log ( '[nocodb.getExpenses] Successfully fetched expenses, count:' , result . list ? . length || 0 ) ;
// Transform expenses to add computed price numbers
if ( result . list && Array . isArray ( result . list ) ) {
result . list = result . list . map ( expense = > ( {
. . . expense ,
// Parse price string to number for calculations
PriceNumber : parseFloat ( expense . Price . replace ( /[€$,]/g , '' ) ) || 0
} ) ) ;
}
return result ;
} catch ( error : any ) {
console . error ( '[nocodb.getExpenses] Error fetching expenses:' , error ) ;
console . error ( '[nocodb.getExpenses] Error details:' , error instanceof Error ? error . message : 'Unknown error' ) ;
throw error ;
}
} ;
export const getExpenseById = async ( id : string ) = > {
console . log ( '[nocodb.getExpenseById] Fetching expense ID:' , id ) ;
try {
const result = await $fetch < Expense > ( ` ${ createTableUrl ( Table . Expense ) } / ${ id } ` , {
headers : {
"xc-token" : getNocoDbConfiguration ( ) . token ,
} ,
} ) ;
console . log ( '[nocodb.getExpenseById] Successfully fetched expense:' , result . Id ) ;
// Add computed price number
const expenseWithPrice = {
. . . result ,
PriceNumber : parseFloat ( result . Price . replace ( /[€$,]/g , '' ) ) || 0
} ;
return expenseWithPrice ;
} catch ( error : any ) {
console . error ( '[nocodb.getExpenseById] Error fetching expense:' , error ) ;
console . error ( '[nocodb.getExpenseById] Error details:' , error instanceof Error ? error . message : 'Unknown error' ) ;
throw error ;
}
} ;
// Helper function to get current month expenses (default view)
export const getCurrentMonthExpenses = async ( ) = > {
const now = new Date ( ) ;
const startOfMonth = new Date ( now . getFullYear ( ) , now . getMonth ( ) , 1 ) . toISOString ( ) . slice ( 0 , 10 ) ;
const endOfMonth = new Date ( now . getFullYear ( ) , now . getMonth ( ) + 1 , 0 ) . toISOString ( ) . slice ( 0 , 10 ) ;
console . log ( '[nocodb.getCurrentMonthExpenses] Fetching current month expenses:' , startOfMonth , 'to' , endOfMonth ) ;
return getExpenses ( {
startDate : startOfMonth ,
endDate : endOfMonth
} ) ;
} ;
// Helper function to group expenses by payer
export const groupExpensesByPayer = ( expenses : Expense [ ] ) = > {
const groups = expenses . reduce ( ( acc , expense ) = > {
const payer = expense . Payer || 'Unknown' ;
if ( ! acc [ payer ] ) {
acc [ payer ] = {
name : payer ,
expenses : [ ] ,
count : 0 ,
total : 0
} ;
}
acc [ payer ] . expenses . push ( expense ) ;
acc [ payer ] . count ++ ;
acc [ payer ] . total += parseFloat ( expense . Price . replace ( /[€$,]/g , '' ) ) || 0 ;
return acc ;
} , { } as Record < string , { name : string ; expenses : Expense [ ] ; count : number ; total : number } > ) ;
return Object . values ( groups ) ;
} ;