From bc0fa6fbe063119a6b6203dab9fc115014b3db60 Mon Sep 17 00:00:00 2001
From: Ron
Date: Tue, 3 Jun 2025 22:04:22 +0300
Subject: [PATCH] feat: add interest button
---
app.vue | 1 +
components/CreateInterestModal.vue | 347 ++++++++++++++++++++++++++++
components/GlobalToast.vue | 31 +++
components/InterestDetailsModal.vue | 21 +-
components/InterestSalesBadge.vue | 7 +-
composables/useToast.ts | 51 ++++
pages/dashboard/interest-list.vue | 28 ++-
pages/dashboard/interest-status.vue | 86 ++++++-
server/api/create-interest.ts | 29 +++
server/utils/nocodb.ts | 92 ++++++--
10 files changed, 653 insertions(+), 40 deletions(-)
create mode 100644 components/CreateInterestModal.vue
create mode 100644 components/GlobalToast.vue
create mode 100644 composables/useToast.ts
create mode 100644 server/api/create-interest.ts
diff --git a/app.vue b/app.vue
index 2ecd34f..7040953 100644
--- a/app.vue
+++ b/app.vue
@@ -1,4 +1,5 @@
+
diff --git a/components/CreateInterestModal.vue b/components/CreateInterestModal.vue
new file mode 100644
index 0000000..fca2424
--- /dev/null
+++ b/components/CreateInterestModal.vue
@@ -0,0 +1,347 @@
+
+
+
+
+
+ mdi-account-plus
+ Create New Interest
+
+
+
+ mdi-close
+
+
+
+
+
+ Berths and berth recommendations can only be assigned after creating the interest.
+
+
+
+
+
+ mdi-account-circle
+ Contact Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-sail-boat
+ Yacht Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-chart-line
+ Sales Information
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-information
+ Additional Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ mdi-content-save
+ Create Interest
+
+
+
+
+
+
+
+
+
diff --git a/components/GlobalToast.vue b/components/GlobalToast.vue
new file mode 100644
index 0000000..87a723d
--- /dev/null
+++ b/components/GlobalToast.vue
@@ -0,0 +1,31 @@
+
+
+
+ mdi-check-circle
+ mdi-alert-circle
+ mdi-alert
+ mdi-information
+ {{ toast.message }}
+
+
+
+ Close
+
+
+
+
+
+
diff --git a/components/InterestDetailsModal.vue b/components/InterestDetailsModal.vue
index f66747a..ad42e17 100644
--- a/components/InterestDetailsModal.vue
+++ b/components/InterestDetailsModal.vue
@@ -454,6 +454,7 @@ const emit = defineEmits();
const { mobile } = useDisplay();
const user = useDirectusUser();
+const toast = useToast();
// Local copy of the interest for editing
const interest = ref(null);
@@ -528,12 +529,12 @@ const saveInterest = async () => {
},
});
- alert("Interest saved successfully!");
+ toast.success("Interest saved successfully!");
emit("save", interest.value);
closeModal();
} catch (error) {
console.error("Failed to save interest:", error);
- alert("Failed to save interest. Please try again.");
+ toast.error("Failed to save interest. Please try again.");
} finally {
isSaving.value = false;
}
@@ -555,11 +556,11 @@ const requestMoreInfoToSales = async () => {
},
});
- alert("Request More Info - To Sales sent successfully!");
+ toast.success("Request More Info - To Sales sent successfully!");
emit("requestMoreInfoToSales", interest.value);
} catch (error) {
console.error("Failed to send request:", error);
- alert("Failed to send request. Please try again.");
+ toast.error("Failed to send request. Please try again.");
} finally {
isRequestingMoreInfo.value = false;
}
@@ -581,11 +582,11 @@ const requestMoreInformation = async () => {
},
});
- alert("Request More Information sent successfully!");
+ toast.success("Request More Information sent successfully!");
emit("requestMoreInformation", interest.value);
} catch (error) {
console.error("Failed to send request:", error);
- alert("Failed to send request. Please try again.");
+ toast.error("Failed to send request. Please try again.");
} finally {
isRequestingMoreInformation.value = false;
}
@@ -607,11 +608,11 @@ const eoiSendToSales = async () => {
},
});
- alert("EOI Send to Sales sent successfully!");
+ toast.success("EOI Send to Sales sent successfully!");
emit("eoiSendToSales", interest.value);
} catch (error) {
console.error("Failed to send EOI:", error);
- alert("Failed to send EOI. Please try again.");
+ toast.error("Failed to send EOI. Please try again.");
} finally {
isSendingEOI.value = false;
}
@@ -725,7 +726,7 @@ const updateBerths = async (newBerths: number[]) => {
originalBerths.value = [...newBerths];
} catch (error) {
console.error("Failed to update berths:", error);
- alert("Failed to update berths. Please try again.");
+ toast.error("Failed to update berths. Please try again.");
// Revert to original values on error
selectedBerths.value = [...originalBerths.value];
}
@@ -776,7 +777,7 @@ const updateBerthRecommendations = async (newRecommendations: number[]) => {
originalBerthRecommendations.value = [...newRecommendations];
} catch (error) {
console.error("Failed to update berth recommendations:", error);
- alert("Failed to update berth recommendations. Please try again.");
+ toast.error("Failed to update berth recommendations. Please try again.");
// Revert to original values on error
selectedBerthRecommendations.value = [
...originalBerthRecommendations.value,
diff --git a/components/InterestSalesBadge.vue b/components/InterestSalesBadge.vue
index 01fbb42..e71a9e8 100644
--- a/components/InterestSalesBadge.vue
+++ b/components/InterestSalesBadge.vue
@@ -1,6 +1,6 @@
- {{ salesProcessLevel }}
+ {{ salesProcessLevel || 'No Status' }}
@@ -18,7 +18,7 @@ type InterestSalesProcessLevel =
| "Contract Signed";
const props = defineProps<{
- salesProcessLevel: InterestSalesProcessLevel;
+ salesProcessLevel: InterestSalesProcessLevel | null | undefined;
}>();
const colorMapping: Record = {
@@ -33,7 +33,8 @@ const colorMapping: Record = {
};
const badgeColor = computed(() => {
- return colorMapping[props.salesProcessLevel] || "grey";
+ if (!props.salesProcessLevel) return "grey";
+ return colorMapping[props.salesProcessLevel as InterestSalesProcessLevel] || "grey";
});
diff --git a/composables/useToast.ts b/composables/useToast.ts
new file mode 100644
index 0000000..dc3b62a
--- /dev/null
+++ b/composables/useToast.ts
@@ -0,0 +1,51 @@
+import { ref } from 'vue'
+
+interface Toast {
+ show: boolean
+ message: string
+ color: string
+ timeout: number
+}
+
+const toast = ref({
+ show: false,
+ message: '',
+ color: 'success',
+ timeout: 3000
+})
+
+export const useToast = () => {
+ const showToast = (message: string, color: string = 'success', timeout: number = 3000) => {
+ toast.value = {
+ show: true,
+ message,
+ color,
+ timeout
+ }
+ }
+
+ const success = (message: string) => {
+ showToast(message, 'success')
+ }
+
+ const error = (message: string) => {
+ showToast(message, 'error')
+ }
+
+ const info = (message: string) => {
+ showToast(message, 'info')
+ }
+
+ const warning = (message: string) => {
+ showToast(message, 'warning')
+ }
+
+ return {
+ toast,
+ showToast,
+ success,
+ error,
+ info,
+ warning
+ }
+}
diff --git a/pages/dashboard/interest-list.vue b/pages/dashboard/interest-list.vue
index 0a6a7d1..5178981 100644
--- a/pages/dashboard/interest-list.vue
+++ b/pages/dashboard/interest-list.vue
@@ -3,7 +3,7 @@
-
+
mdi-account-group
Interest List
@@ -12,6 +12,17 @@
Manage and track all potential client interests
+
+
+ Add Interest
+
+
@@ -182,6 +193,12 @@
:selected-interest="selectedInterest"
@save="handleSaveInterest"
/>
+
+
+
@@ -189,6 +206,7 @@
import LeadCategoryBadge from "~/components/LeadCategoryBadge.vue";
import InterestSalesBadge from "~/components/InterestSalesBadge.vue";
import InterestDetailsModal from "~/components/InterestDetailsModal.vue";
+import CreateInterestModal from "~/components/CreateInterestModal.vue";
import { useFetch } from "#app";
import { ref, computed } from "vue";
import type { Interest } from "@/utils/types";
@@ -201,6 +219,7 @@ const user = useDirectusUser();
const router = useRouter();
const loading = ref(true);
const showModal = ref(false);
+const showCreateModal = ref(false);
const selectedInterest = ref(null);
const selectedSalesLevel = ref('all');
@@ -231,6 +250,13 @@ const handleSaveInterest = async (interest: Interest) => {
loading.value = false;
};
+const handleInterestCreated = async (interest: Interest) => {
+ // Refresh the interests data to include the new interest
+ loading.value = true;
+ await refresh();
+ loading.value = false;
+};
+
const headers = [
{ title: "Contact", key: "Full Name", sortable: true, width: "25%" },
{ title: "Yacht", key: "Yacht Name", sortable: true },
diff --git a/pages/dashboard/interest-status.vue b/pages/dashboard/interest-status.vue
index 73fa76d..b49d4bb 100644
--- a/pages/dashboard/interest-status.vue
+++ b/pages/dashboard/interest-status.vue
@@ -1,8 +1,21 @@
+
+
+
+
+
-
+
mdi-view-column
Interest Status Board
@@ -11,6 +24,17 @@
Track interests through the sales pipeline
+
+
+ Add Interest
+
+
@@ -148,13 +172,20 @@
:selected-interest="selectedInterest"
@save="handleSaveInterest"
/>
+
+
+
diff --git a/server/api/create-interest.ts b/server/api/create-interest.ts
new file mode 100644
index 0000000..1929cba
--- /dev/null
+++ b/server/api/create-interest.ts
@@ -0,0 +1,29 @@
+import { createInterest } from "../utils/nocodb";
+
+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);
+
+ if (!body || Object.keys(body).length === 0) {
+ throw createError({ statusCode: 400, statusMessage: "No data provided" });
+ }
+
+ const createdInterest = await createInterest(body);
+ return createdInterest;
+ } 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 a96e0da..d4d8cff 100644
--- a/server/utils/nocodb.ts
+++ b/server/utils/nocodb.ts
@@ -41,39 +41,103 @@ 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"
+ "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',
+ method: "PATCH",
headers: {
"xc-token": getNocoDbConfiguration().token,
},
body: {
- Id: id, // This identifies the record to update
- ...cleanData // These are the fields to update
+ Id: id, // This identifies the record to update
+ ...cleanData, // These are the fields to update
},
});
};
+export const createInterest = async (data: Partial) => {
+ // 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",
+ "Place of Residence",
+ "Contact Method Preferred",
+ "Lead Category",
+ ];
+
+ // Filter the data to only include allowed fields
+ for (const field of allowedFields) {
+ if (field in data) {
+ cleanData[field] = data[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;
+
+ return $fetch(createTableUrl(Table.Interest), {
+ method: "POST",
+ headers: {
+ "xc-token": getNocoDbConfiguration().token,
+ },
+ body: cleanData,
+ });
+};
+
export const triggerWebhook = async (url: string, payload: any) =>
$fetch(url, {
- method: 'POST',
+ method: "POST",
body: payload,
});