feat: add interest button
This commit is contained in:
347
components/CreateInterestModal.vue
Normal file
347
components/CreateInterestModal.vue
Normal file
@@ -0,0 +1,347 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="isOpen"
|
||||
max-width="800"
|
||||
persistent
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar dark color="primary">
|
||||
<v-toolbar-title>
|
||||
<v-icon class="mr-2">mdi-account-plus</v-icon>
|
||||
Create New Interest
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon dark @click="closeModal">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
Berths and berth recommendations can only be assigned after creating the interest.
|
||||
</v-alert>
|
||||
|
||||
<v-form ref="form" @submit.prevent="createInterest">
|
||||
<!-- Contact Information Section -->
|
||||
<div class="text-h6 mb-3">
|
||||
<v-icon class="mr-2" color="primary">mdi-account-circle</v-icon>
|
||||
Contact Information
|
||||
</div>
|
||||
<v-row dense class="mb-4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest['Full Name']"
|
||||
label="Full Name *"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-account"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest['Email Address']"
|
||||
label="Email Address *"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-email"
|
||||
:rules="[rules.required, rules.email]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest['Phone Number']"
|
||||
label="Phone Number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-phone"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="newInterest['Contact Method Preferred']"
|
||||
label="Preferred Contact Method"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="ContactMethodPreferredFlow"
|
||||
prepend-inner-icon="mdi-message-text"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest.Address"
|
||||
label="Address"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-map-marker"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest['Place of Residence']"
|
||||
label="Place of Residence"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-home"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Yacht Information Section -->
|
||||
<div class="text-h6 mb-3">
|
||||
<v-icon class="mr-2" color="primary">mdi-sail-boat</v-icon>
|
||||
Yacht Information
|
||||
</div>
|
||||
<v-row dense class="mb-4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest['Yacht Name']"
|
||||
label="Yacht Name"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-ferry"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest['Berth Size Desired']"
|
||||
label="Berth Size Desired"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-tape-measure"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="newInterest.Length"
|
||||
label="Length"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-ruler"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="newInterest.Width"
|
||||
label="Width"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-arrow-expand-horizontal"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="newInterest.Depth"
|
||||
label="Depth"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-arrow-expand-vertical"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Sales Information Section -->
|
||||
<div class="text-h6 mb-3">
|
||||
<v-icon class="mr-2" color="primary">mdi-chart-line</v-icon>
|
||||
Sales Information
|
||||
</div>
|
||||
<v-row dense class="mb-4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="newInterest['Sales Process Level']"
|
||||
label="Sales Process Level *"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="InterestSalesProcessLevelFlow"
|
||||
prepend-inner-icon="mdi-chart-line"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="newInterest['Lead Category']"
|
||||
label="Lead Category"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="InterestLeadCategoryFlow"
|
||||
prepend-inner-icon="mdi-tag"
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Additional Information Section -->
|
||||
<div class="text-h6 mb-3">
|
||||
<v-icon class="mr-2" color="primary">mdi-information</v-icon>
|
||||
Additional Information
|
||||
</div>
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest.Source"
|
||||
label="Source"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-source-branch"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest['Extra Comments']"
|
||||
label="Extra Comments"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
prepend-inner-icon="mdi-comment-text"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="closeModal"
|
||||
:disabled="isCreating"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
@click="createInterest"
|
||||
:loading="isCreating"
|
||||
:disabled="isCreating"
|
||||
>
|
||||
<v-icon start>mdi-content-save</v-icon>
|
||||
Create Interest
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import type { Interest } from "@/utils/types";
|
||||
import {
|
||||
InterestSalesProcessLevelFlow,
|
||||
InterestLeadCategoryFlow,
|
||||
ContactMethodPreferredFlow,
|
||||
} from "@/utils/types";
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "created", interest: Interest): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const user = useDirectusUser();
|
||||
const toast = useToast();
|
||||
|
||||
// Form ref
|
||||
const form = ref();
|
||||
|
||||
// Loading state
|
||||
const isCreating = ref(false);
|
||||
|
||||
// Initial interest data
|
||||
const getInitialInterest = () => ({
|
||||
"Full Name": "",
|
||||
"Email Address": "",
|
||||
"Phone Number": "",
|
||||
"Contact Method Preferred": "Email",
|
||||
Address: "",
|
||||
"Place of Residence": "",
|
||||
"Yacht Name": "",
|
||||
"Berth Size Desired": "",
|
||||
Length: "",
|
||||
Width: "",
|
||||
Depth: "",
|
||||
"Sales Process Level": "General Qualified Interest",
|
||||
"Lead Category": "General",
|
||||
Source: "",
|
||||
"Extra Comments": "",
|
||||
"Date Added": new Date().toLocaleDateString("en-GB").replace(/\//g, "-"),
|
||||
"Created At": new Date().toLocaleDateString("en-GB").replace(/\//g, "-"),
|
||||
});
|
||||
|
||||
// New interest data
|
||||
const newInterest = ref(getInitialInterest());
|
||||
|
||||
// Validation rules
|
||||
const rules = {
|
||||
required: (v: any) => !!v || "This field is required",
|
||||
email: (v: string) => {
|
||||
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return pattern.test(v) || "Invalid email address";
|
||||
},
|
||||
};
|
||||
|
||||
// Computed property for v-model binding
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
// Reset form when dialog opens
|
||||
watch(isOpen, (newValue) => {
|
||||
if (newValue) {
|
||||
newInterest.value = getInitialInterest();
|
||||
form.value?.resetValidation();
|
||||
}
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
|
||||
const createInterest = async () => {
|
||||
// Validate form
|
||||
const { valid } = await form.value.validate();
|
||||
if (!valid) {
|
||||
toast.error("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
isCreating.value = true;
|
||||
try {
|
||||
// Call the create-interest API
|
||||
const response = await $fetch<Interest>("/api/create-interest", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-tag": user.value?.email ? "094ut234" : "pjnvü1230",
|
||||
},
|
||||
body: newInterest.value,
|
||||
});
|
||||
|
||||
toast.success("Interest created successfully!");
|
||||
emit("created", response);
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
console.error("Failed to create interest:", error);
|
||||
toast.error("Failed to create interest. Please try again.");
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-alert {
|
||||
border-left: 4px solid rgb(var(--v-theme-info));
|
||||
}
|
||||
</style>
|
||||
31
components/GlobalToast.vue
Normal file
31
components/GlobalToast.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="toast.show"
|
||||
:color="toast.color"
|
||||
:timeout="toast.timeout"
|
||||
location="top"
|
||||
variant="flat"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon v-if="toast.color === 'success'" class="mr-2">mdi-check-circle</v-icon>
|
||||
<v-icon v-else-if="toast.color === 'error'" class="mr-2">mdi-alert-circle</v-icon>
|
||||
<v-icon v-else-if="toast.color === 'warning'" class="mr-2">mdi-alert</v-icon>
|
||||
<v-icon v-else-if="toast.color === 'info'" class="mr-2">mdi-information</v-icon>
|
||||
<span>{{ toast.message }}</span>
|
||||
</div>
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="toast.show = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useToast } from '~/composables/useToast'
|
||||
|
||||
const { toast } = useToast()
|
||||
</script>
|
||||
@@ -454,6 +454,7 @@ const emit = defineEmits<Emits>();
|
||||
const { mobile } = useDisplay();
|
||||
|
||||
const user = useDirectusUser();
|
||||
const toast = useToast();
|
||||
|
||||
// Local copy of the interest for editing
|
||||
const interest = ref<Interest | null>(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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-chip :color="badgeColor" small>
|
||||
{{ salesProcessLevel }}
|
||||
{{ salesProcessLevel || 'No Status' }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
@@ -18,7 +18,7 @@ type InterestSalesProcessLevel =
|
||||
| "Contract Signed";
|
||||
|
||||
const props = defineProps<{
|
||||
salesProcessLevel: InterestSalesProcessLevel;
|
||||
salesProcessLevel: InterestSalesProcessLevel | null | undefined;
|
||||
}>();
|
||||
|
||||
const colorMapping: Record<InterestSalesProcessLevel, string> = {
|
||||
@@ -33,7 +33,8 @@ const colorMapping: Record<InterestSalesProcessLevel, string> = {
|
||||
};
|
||||
|
||||
const badgeColor = computed(() => {
|
||||
return colorMapping[props.salesProcessLevel] || "grey";
|
||||
if (!props.salesProcessLevel) return "grey";
|
||||
return colorMapping[props.salesProcessLevel as InterestSalesProcessLevel] || "grey";
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user