diff --git a/components/InterestDetailsModal.vue b/components/InterestDetailsModal.vue new file mode 100644 index 0000000..a5371be --- /dev/null +++ b/components/InterestDetailsModal.vue @@ -0,0 +1,757 @@ + + + + + + mdi-close + + + mdi-account-details + Interest Details + + + + + mdi-information-outline + Info to Sales + + + mdi-email-outline + Request Info + + + mdi-send + EOI to Sales + + + mdi-content-save + Save Changes + + + + + + + + + + + + + + + + + + + + Created: {{ formatDate(interest['Created At']) }} + + + Request Form Sent: {{ formatDate(interest['Request Form Sent']) }} + + + EOI Sent: {{ formatDate(interest['EOI Time Sent']) }} + + + LOI Sent: {{ formatDate(interest['Time LOI Sent']) }} + + + + + + + + + + mdi-account-circle + Contact Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mdi-sail-boat + Yacht Information + + + + + + + + + + + + + + + + + + + + + + mdi-anchor + Berth & Sales Information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + mdi-information + Additional Information + + + + + + + + + + + + mdi-file-document + EOI Documents: + + + {{ doc.title || `EOI Document ${index + 1}` }} + + + + + + + + + + + + + + diff --git a/pages/dashboard/interest-list.vue b/pages/dashboard/interest-list.vue new file mode 100644 index 0000000..b66913f --- /dev/null +++ b/pages/dashboard/interest-list.vue @@ -0,0 +1,438 @@ + + + + + + + + mdi-account-group + Interest List + + + Manage and track all potential client interests + + + + + + + + + + + + {{ filteredInterests.length }} results + + + + + + + + + All Levels + + + Qualified + + + LOI Sent + + + Reserved + + + + + + + + + + + + + + + {{ getInitials(item["Full Name"]) }} + + + + {{ item["Full Name"] }} + {{ item["Email Address"] }} + + + + + + mdi-sail-boat + {{ item["Yacht Name"] }} + + — + + + + {{ item["Berth Number"] }} + + — + + + + + + + mdi-phone + {{ item["Phone Number"] }} + + — + + + + + {{ formatDate(item["Created At"]) }} + {{ getRelativeTime(item["Created At"]) }} + + + + + + + mdi-comment-text + Note + + + {{ item["Extra Comments"] }} + + — + + + + + + + + + + + + + + + + + diff --git a/pages/dashboard/interest-list/[id].vue b/pages/dashboard/interest-list/[id].vue deleted file mode 100644 index a09ab7a..0000000 --- a/pages/dashboard/interest-list/[id].vue +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - Back to Contacts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Request More Info - To Sales - Request More Information - EOI Send to Sales - - Save - - - - - - - - - - diff --git a/pages/dashboard/interest-list/index.vue b/pages/dashboard/interest-list/index.vue deleted file mode 100644 index 309e4f6..0000000 --- a/pages/dashboard/interest-list/index.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - {{ item["Full Name"] }} - {{ item["Yacht Name"] }} - {{ item.Length }} - {{ item.Width }} - {{ item.Depth }} - {{ item["Berth Number"] }} - - - - {{ item["Phone Number"] }} - {{ item["Email Address"] }} - - - - - mdi-comment - - {{ item["Extra Comments"] }} - - - - - - - - - - - - diff --git a/pages/dashboard/interest-status.vue b/pages/dashboard/interest-status.vue index 04776c8..b7f8c17 100644 --- a/pages/dashboard/interest-status.vue +++ b/pages/dashboard/interest-status.vue @@ -1,85 +1,181 @@ - - - + + + + + + mdi-view-column + Interest Status Board + + + Track interests through the sales pipeline + + + + + + + - - - - - + + + + + {{ getColumnIcon(level) }} + {{ level }} + + {{ groupedInterests[level].length }} + + + + + + - {{ interest["Full Name"] }} - - - - Yacht Name - {{ - interest["Yacht Name"] || "-" - }} - - - Email Address - {{ - interest["Email Address"] || "-" - }} - - - Berth Number - {{ - interest["Berth Number"] || "-" - }} - - - Phone Number - {{ - interest["Phone Number"] || "-" - }} - - - Berth Size Desired - {{ - interest["Berth Size Desired"] || "-" - }} - - + + + + + + {{ getInitials(interest["Full Name"]) }} + + + + {{ interest["Full Name"] }} + {{ interest["Yacht Name"] || "No yacht specified" }} + + + + + + + mdi-email + {{ interest["Email Address"] }} + + + mdi-phone + {{ interest["Phone Number"] }} + + + + + + + mdi-anchor + {{ interest["Berth Number"] }} + + + mdi-ruler + {{ interest["Berth Size Desired"] }} + + + + + + + mdi-comment + Has notes + + + + + + mdi-view-column-outline + No interests to display + Interests will appear here as they progress through the sales pipeline + - - + + + + + diff --git a/server/api/eoi-send-to-sales.ts b/server/api/eoi-send-to-sales.ts new file mode 100644 index 0000000..6cfa971 --- /dev/null +++ b/server/api/eoi-send-to-sales.ts @@ -0,0 +1,50 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + try { + const body = await readBody(event); + const { interestId } = body; + + if (!interestId) { + throw createError({ statusCode: 400, statusMessage: "Interest ID is required" }); + } + + // Get the interest data + const interest = await getInterestById(interestId); + + // Prepare the webhook payload + const webhookPayload = { + "type": "records.after.trigger", + "id": crypto.randomUUID(), // Generate a random UUID + "data": { + "table_id": "mbs9hjauug4eseo", + "table_name": "Interests", + "rows": [interest] + } + }; + + // Trigger the webhook + const webhookUrl = "https://automation.portnimara.com/api/v1/webhooks/cCRKsqPB9AHuj4XjFFiPr"; + await triggerWebhook(webhookUrl, webhookPayload); + + // Update the interest to mark that the EOI was sent + await updateInterest(interestId, { + "EOI Send to Sales": new Date().toISOString() + }); + + return { success: true, message: "EOI send to sales triggered successfully" }; + } catch (error) { + if (error instanceof Error) { + throw createError({ statusCode: 500, statusMessage: error.message }); + } else { + throw createError({ + statusCode: 500, + statusMessage: "An unexpected error occurred", + }); + } + } +}); diff --git a/server/api/get-berths.ts b/server/api/get-berths.ts new file mode 100644 index 0000000..8b5e4c8 --- /dev/null +++ b/server/api/get-berths.ts @@ -0,0 +1,21 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + const config = getNocoDbConfiguration(); + const berthsTableId = "mczgos9hr3oa9qc"; + + const berths = await $fetch(`${config.url}/api/v2/tables/${berthsTableId}/records`, { + headers: { + "xc-token": config.token, + }, + params: { + limit: 1000, + }, + }); + + return berths; +}); diff --git a/server/api/get-interest-berths.ts b/server/api/get-interest-berths.ts new file mode 100644 index 0000000..2722ec8 --- /dev/null +++ b/server/api/get-interest-berths.ts @@ -0,0 +1,47 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + const query = getQuery(event); + const { interestId, linkType } = query; + + if (!interestId || !linkType) { + throw createError({ + statusCode: 400, + statusMessage: "interestId and linkType are required" + }); + } + + const config = getNocoDbConfiguration(); + const interestsTableId = "mbs9hjauug4eseo"; + + // Determine which link field to use + let linkFieldId; + if (linkType === 'berths') { + linkFieldId = "cj7v7bb9pa5eyo3"; // Berths field + } else if (linkType === 'recommendations') { + linkFieldId = "cgthyq2e95ajc52"; // Berth Recommendations field + } else { + throw createError({ + statusCode: 400, + statusMessage: "linkType must be 'berths' or 'recommendations'" + }); + } + + const result = await $fetch( + `${config.url}/api/v2/tables/${interestsTableId}/links/${linkFieldId}/records/${interestId}`, + { + headers: { + "xc-token": config.token, + }, + params: { + limit: 1000, + }, + } + ); + + return result; +}); diff --git a/server/api/link-berth-recommendations-to-interest.ts b/server/api/link-berth-recommendations-to-interest.ts new file mode 100644 index 0000000..29e7cbb --- /dev/null +++ b/server/api/link-berth-recommendations-to-interest.ts @@ -0,0 +1,37 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + const body = await readBody(event); + const { interestId, berthIds } = body; + + if (!interestId || !berthIds || !Array.isArray(berthIds)) { + throw createError({ + statusCode: 400, + statusMessage: "interestId and berthIds array are required" + }); + } + + const config = getNocoDbConfiguration(); + const interestsTableId = "mbs9hjauug4eseo"; + const berthRecommendationsLinkFieldId = "cgthyq2e95ajc52"; // Berth Recommendations field + + // Format the berth IDs for the API + const berthRecords = berthIds.map(id => ({ Id: id })); + + const result = await $fetch( + `${config.url}/api/v2/tables/${interestsTableId}/links/${berthRecommendationsLinkFieldId}/records/${interestId}`, + { + method: 'POST', + headers: { + "xc-token": config.token, + }, + body: berthRecords, + } + ); + + return result; +}); diff --git a/server/api/link-berths-to-interest.ts b/server/api/link-berths-to-interest.ts new file mode 100644 index 0000000..bfc6890 --- /dev/null +++ b/server/api/link-berths-to-interest.ts @@ -0,0 +1,37 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + const body = await readBody(event); + const { interestId, berthIds } = body; + + if (!interestId || !berthIds || !Array.isArray(berthIds)) { + throw createError({ + statusCode: 400, + statusMessage: "interestId and berthIds array are required" + }); + } + + const config = getNocoDbConfiguration(); + const interestsTableId = "mbs9hjauug4eseo"; + const berthsLinkFieldId = "cj7v7bb9pa5eyo3"; // Berths field + + // Format the berth IDs for the API + const berthRecords = berthIds.map(id => ({ Id: id })); + + const result = await $fetch( + `${config.url}/api/v2/tables/${interestsTableId}/links/${berthsLinkFieldId}/records/${interestId}`, + { + method: 'POST', + headers: { + "xc-token": config.token, + }, + body: berthRecords, + } + ); + + return result; +}); diff --git a/server/api/request-more-info-to-sales.ts b/server/api/request-more-info-to-sales.ts new file mode 100644 index 0000000..3058d20 --- /dev/null +++ b/server/api/request-more-info-to-sales.ts @@ -0,0 +1,50 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + try { + const body = await readBody(event); + const { interestId } = body; + + if (!interestId) { + throw createError({ statusCode: 400, statusMessage: "Interest ID is required" }); + } + + // Get the interest data + const interest = await getInterestById(interestId); + + // Prepare the webhook payload + const webhookPayload = { + "type": "records.after.trigger", + "id": crypto.randomUUID(), // Generate a random UUID + "data": { + "table_id": "mbs9hjauug4eseo", + "table_name": "Interests", + "rows": [interest] + } + }; + + // Trigger the webhook + const webhookUrl = "https://automation.portnimara.com/api/v1/webhooks/vEWQdpe4CXS24E86tV2Cb"; + await triggerWebhook(webhookUrl, webhookPayload); + + // Update the interest to mark that the request was sent + await updateInterest(interestId, { + "Request More Info - To Sales": new Date().toISOString() + }); + + return { success: true, message: "Request more info to sales triggered successfully" }; + } catch (error) { + if (error instanceof Error) { + throw createError({ statusCode: 500, statusMessage: error.message }); + } else { + throw createError({ + statusCode: 500, + statusMessage: "An unexpected error occurred", + }); + } + } +}); diff --git a/server/api/request-more-information.ts b/server/api/request-more-information.ts new file mode 100644 index 0000000..840144d --- /dev/null +++ b/server/api/request-more-information.ts @@ -0,0 +1,50 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + try { + const body = await readBody(event); + const { interestId } = body; + + if (!interestId) { + throw createError({ statusCode: 400, statusMessage: "Interest ID is required" }); + } + + // Get the interest data + const interest = await getInterestById(interestId); + + // Prepare the webhook payload + const webhookPayload = { + "type": "records.after.trigger", + "id": crypto.randomUUID(), // Generate a random UUID + "data": { + "table_id": "mbs9hjauug4eseo", + "table_name": "Interests", + "rows": [interest] + } + }; + + // Trigger the webhook + const webhookUrl = "https://automation.portnimara.com/api/v1/webhooks/B6lnXZoospLXcVJJTABh0"; + await triggerWebhook(webhookUrl, webhookPayload); + + // Update the interest to mark that the request was sent + await updateInterest(interestId, { + "Request More Information": new Date().toISOString() + }); + + return { success: true, message: "Request more information triggered successfully" }; + } catch (error) { + if (error instanceof Error) { + throw createError({ statusCode: 500, statusMessage: error.message }); + } else { + throw createError({ + statusCode: 500, + statusMessage: "An unexpected error occurred", + }); + } + } +}); diff --git a/server/api/unlink-berth-recommendations-from-interest.ts b/server/api/unlink-berth-recommendations-from-interest.ts new file mode 100644 index 0000000..abe6ad2 --- /dev/null +++ b/server/api/unlink-berth-recommendations-from-interest.ts @@ -0,0 +1,37 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + const body = await readBody(event); + const { interestId, berthIds } = body; + + if (!interestId || !berthIds || !Array.isArray(berthIds)) { + throw createError({ + statusCode: 400, + statusMessage: "interestId and berthIds array are required" + }); + } + + const config = getNocoDbConfiguration(); + const interestsTableId = "mbs9hjauug4eseo"; + const berthRecommendationsLinkFieldId = "cgthyq2e95ajc52"; // Berth Recommendations field + + // Format the berth IDs for the API + const berthRecords = berthIds.map(id => ({ Id: id })); + + const result = await $fetch( + `${config.url}/api/v2/tables/${interestsTableId}/links/${berthRecommendationsLinkFieldId}/records/${interestId}`, + { + method: 'DELETE', + headers: { + "xc-token": config.token, + }, + body: berthRecords, + } + ); + + return result; +}); diff --git a/server/api/unlink-berths-from-interest.ts b/server/api/unlink-berths-from-interest.ts new file mode 100644 index 0000000..829daaf --- /dev/null +++ b/server/api/unlink-berths-from-interest.ts @@ -0,0 +1,37 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + const body = await readBody(event); + const { interestId, berthIds } = body; + + if (!interestId || !berthIds || !Array.isArray(berthIds)) { + throw createError({ + statusCode: 400, + statusMessage: "interestId and berthIds array are required" + }); + } + + const config = getNocoDbConfiguration(); + const interestsTableId = "mbs9hjauug4eseo"; + const berthsLinkFieldId = "cj7v7bb9pa5eyo3"; // Berths field + + // Format the berth IDs for the API + const berthRecords = berthIds.map(id => ({ Id: id })); + + const result = await $fetch( + `${config.url}/api/v2/tables/${interestsTableId}/links/${berthsLinkFieldId}/records/${interestId}`, + { + method: 'DELETE', + headers: { + "xc-token": config.token, + }, + body: berthRecords, + } + ); + + return result; +}); diff --git a/server/api/update-interest.ts b/server/api/update-interest.ts new file mode 100644 index 0000000..b4ee09d --- /dev/null +++ b/server/api/update-interest.ts @@ -0,0 +1,38 @@ +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + if (!xTagHeader || xTagHeader !== "094ut234") { + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + try { + const body = await readBody(event); + const { id, data } = body; + + if (!id) { + throw createError({ statusCode: 400, statusMessage: "ID is required" }); + } + + if (!data || Object.keys(data).length === 0) { + throw createError({ statusCode: 400, statusMessage: "No data provided for update" }); + } + + // Remove Id from data if it exists to avoid duplication + const updateData = { ...data }; + if ('Id' in updateData) { + delete updateData.Id; + } + + const updatedInterest = await updateInterest(id, updateData); + return updatedInterest; + } catch (error) { + if (error instanceof Error) { + throw createError({ statusCode: 500, statusMessage: error.message }); + } else { + throw createError({ + statusCode: 500, + statusMessage: "An unexpected error occurred", + }); + } + } +}); diff --git a/server/utils/nocodb.ts b/server/utils/nocodb.ts index 774f762..a96e0da 100644 --- a/server/utils/nocodb.ts +++ b/server/utils/nocodb.ts @@ -36,3 +36,44 @@ export const getInterestById = async (id: string) => "xc-token": getNocoDbConfiguration().token, }, }); + +export const updateInterest = async (id: string, data: Partial) => { + // 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 + 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", + "Request More Information", "Source", "Place of Residence", + "Contact Method Preferred", "Request Form Sent", "Berth Number", + "EOI Time Sent", "Lead Category", "Request More Info - To Sales", + "EOI Send to Sales", "Time LOI Sent" + ]; + + // Filter the data to only include allowed fields + for (const field of allowedFields) { + if (field in data) { + cleanData[field] = data[field]; + } + } + + return $fetch(createTableUrl(Table.Interest), { + method: 'PATCH', + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + body: { + Id: id, // This identifies the record to update + ...cleanData // These are the fields to update + }, + }); +}; + +export const triggerWebhook = async (url: string, payload: any) => + $fetch(url, { + method: 'POST', + body: payload, + }); diff --git a/utils/types.ts b/utils/types.ts index 7c3fff3..0082f41 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -1,4 +1,5 @@ export interface Berth { + Id: number; "Mooring Number": string; Length: string; Draft: string; @@ -92,4 +93,9 @@ export interface Interest { "Request More Info - To Sales": string; "EOI Send to Sales": string; "Time LOI Sent": string; + Berths: number; +} + +export interface InterestsResponse { + list: Interest[]; }
+ Manage and track all potential client interests +
+ Track interests through the sales pipeline +
Interests will appear here as they progress through the sales pipeline