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 @@ + + + + + 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 @@ + + + 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 @@ @@ -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 @@ 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, });