Add interest deletion and sales pipeline status tracking
- Add delete button with confirmation dialog to InterestDetailsModal - Implement delete-interest API endpoint - Add sales pipeline status section with visual indicators - Update UI states to handle deletion loading states - Add color-coded sales process level selection
This commit is contained in:
parent
4d3935e863
commit
2ea72ef24e
|
|
@ -23,7 +23,8 @@
|
|||
:disabled="
|
||||
isRequestingMoreInfo ||
|
||||
isRequestingMoreInformation ||
|
||||
isSendingEOI
|
||||
isSendingEOI ||
|
||||
isDeleting
|
||||
"
|
||||
>
|
||||
<v-icon start>mdi-information-outline</v-icon>
|
||||
|
|
@ -36,7 +37,8 @@
|
|||
:disabled="
|
||||
isRequestingMoreInfo ||
|
||||
isRequestingMoreInformation ||
|
||||
isSendingEOI
|
||||
isSendingEOI ||
|
||||
isDeleting
|
||||
"
|
||||
>
|
||||
<v-icon start>mdi-email-outline</v-icon>
|
||||
|
|
@ -49,19 +51,37 @@
|
|||
:disabled="
|
||||
isRequestingMoreInfo ||
|
||||
isRequestingMoreInformation ||
|
||||
isSendingEOI
|
||||
isSendingEOI ||
|
||||
isDeleting
|
||||
"
|
||||
>
|
||||
<v-icon start>mdi-send</v-icon>
|
||||
EOI to Sales
|
||||
</v-btn>
|
||||
<v-btn
|
||||
@click="confirmDelete"
|
||||
variant="text"
|
||||
color="error"
|
||||
:loading="isDeleting"
|
||||
:disabled="
|
||||
isRequestingMoreInfo ||
|
||||
isRequestingMoreInformation ||
|
||||
isSendingEOI ||
|
||||
isSaving ||
|
||||
isDeleting
|
||||
"
|
||||
class="ml-4"
|
||||
>
|
||||
<v-icon start>mdi-delete</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="success"
|
||||
size="large"
|
||||
@click="saveInterest"
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
:disabled="isSaving || isDeleting"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-icon start>mdi-content-save</v-icon>
|
||||
|
|
@ -340,11 +360,62 @@
|
|||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<!-- Sales Pipeline Status Section -->
|
||||
<v-card variant="flat" class="mb-6">
|
||||
<v-card-title class="text-h6 d-flex align-center pb-4">
|
||||
<v-icon class="mr-2" color="primary">mdi-chart-timeline-variant</v-icon>
|
||||
Sales Pipeline Status
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-2">
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="interest['Sales Process Level']"
|
||||
label="Current Sales Level"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="InterestSalesProcessLevelFlow"
|
||||
prepend-inner-icon="mdi-chart-line"
|
||||
>
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-chip
|
||||
:color="getSalesLevelColor(item.value)"
|
||||
size="small"
|
||||
>
|
||||
{{ item.value }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item="{ item, props }">
|
||||
<v-list-item v-bind="props">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="getSalesLevelColor(item.value)">
|
||||
mdi-circle
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<div class="mt-4">
|
||||
<v-progress-linear
|
||||
:model-value="(currentStep + 1) / InterestSalesProcessLevelFlow.length * 100"
|
||||
height="10"
|
||||
rounded
|
||||
:color="getSalesLevelColor(interest['Sales Process Level'])"
|
||||
/>
|
||||
<div class="text-caption text-center mt-2">
|
||||
Progress: {{ currentStep + 1 }} of {{ InterestSalesProcessLevelFlow.length }} stages
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Berth & Sales Information Section -->
|
||||
<v-card variant="flat" class="mb-6">
|
||||
<v-card-title class="text-h6 d-flex align-center pb-4">
|
||||
<v-icon class="mr-2" color="primary">mdi-anchor</v-icon>
|
||||
Berth & Sales Information
|
||||
Berth Information
|
||||
</v-card-title>
|
||||
<v-card-text class="pt-2">
|
||||
<v-row dense>
|
||||
|
|
@ -357,16 +428,6 @@
|
|||
prepend-inner-icon="mdi-tape-measure"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="interest['Sales Process Level']"
|
||||
label="Sales Process Level"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="InterestSalesProcessLevelFlow"
|
||||
prepend-inner-icon="mdi-chart-line"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="interest['Lead Category']"
|
||||
|
|
@ -605,6 +666,7 @@ const isSaving = ref(false);
|
|||
const isRequestingMoreInfo = ref(false);
|
||||
const isRequestingMoreInformation = ref(false);
|
||||
const isSendingEOI = ref(false);
|
||||
const isDeleting = ref(false);
|
||||
|
||||
// Berths related data
|
||||
const availableBerths = ref<Berth[]>([]);
|
||||
|
|
@ -962,6 +1024,62 @@ const formatDate = (dateString: string | null | undefined) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Get color for sales level
|
||||
const getSalesLevelColor = (level: string) => {
|
||||
switch (level?.toLowerCase()) {
|
||||
case "initial inquiry":
|
||||
return "grey";
|
||||
case "qualified interest":
|
||||
return "blue";
|
||||
case "eoi sent":
|
||||
return "orange";
|
||||
case "loi sent":
|
||||
return "purple";
|
||||
case "reservation agreement sent":
|
||||
return "green";
|
||||
case "reserved":
|
||||
return "success";
|
||||
default:
|
||||
return "grey";
|
||||
}
|
||||
};
|
||||
|
||||
// Confirm delete
|
||||
const confirmDelete = () => {
|
||||
if (!interest.value) return;
|
||||
|
||||
if (confirm(`Are you sure you want to delete the interest for ${interest.value['Full Name']}? This action cannot be undone.`)) {
|
||||
deleteInterest();
|
||||
}
|
||||
};
|
||||
|
||||
// Delete interest
|
||||
const deleteInterest = async () => {
|
||||
if (!interest.value) return;
|
||||
|
||||
isDeleting.value = true;
|
||||
try {
|
||||
await $fetch("/api/delete-interest", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-tag": user.value?.email ? "094ut234" : "pjnvü1230",
|
||||
},
|
||||
body: {
|
||||
id: interest.value.Id.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
toast.success("Interest deleted successfully!");
|
||||
closeModal();
|
||||
emit("save", interest.value); // Trigger refresh
|
||||
} catch (error) {
|
||||
console.error("Failed to delete interest:", error);
|
||||
toast.error("Failed to delete interest. Please try again.");
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Load berths when component mounts
|
||||
onMounted(() => {
|
||||
loadAvailableBerths();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
<v-row class="mb-6">
|
||||
<v-col>
|
||||
<h1 class="text-h4 font-weight-bold">
|
||||
<v-icon class="mr-2" color="primary">mdi-folder</v-icon>
|
||||
File Browser
|
||||
</h1>
|
||||
<p class="text-subtitle-1 text-grey mt-1">
|
||||
|
|
|
|||
|
|
@ -52,7 +52,17 @@
|
|||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" class="d-flex justify-end align-center">
|
||||
<v-col cols="12" md="6" class="d-flex justify-end align-center gap-2">
|
||||
<v-btn
|
||||
v-if="hasActiveFilters"
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="small"
|
||||
@click="clearAllFilters"
|
||||
prepend-icon="mdi-filter-off"
|
||||
>
|
||||
Clear Filters
|
||||
</v-btn>
|
||||
<v-chip-group
|
||||
v-model="selectedSalesLevel"
|
||||
selected-class="text-primary"
|
||||
|
|
@ -119,23 +129,26 @@
|
|||
{{ getInitials(item["Full Name"]) }}
|
||||
</span>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ item["Full Name"] }}</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<span class="font-weight-medium">{{ item["Full Name"] }}</span>
|
||||
<v-tooltip v-if="item['Extra Comments']" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-icon
|
||||
v-bind="props"
|
||||
size="small"
|
||||
color="orange"
|
||||
>
|
||||
mdi-comment-text
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ item["Extra Comments"] }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<div class="text-caption text-grey-darken-1">{{ item["Email Address"] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<v-chip
|
||||
v-if="item['Berth Number']"
|
||||
size="small"
|
||||
color="deep-purple"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ item["Berth Number"] }}
|
||||
</v-chip>
|
||||
<span v-else class="text-grey-darken-1">—</span>
|
||||
</td>
|
||||
<td>
|
||||
<InterestSalesBadge
|
||||
:salesProcessLevel="item['Sales Process Level']"
|
||||
|
|
@ -148,27 +161,6 @@
|
|||
/>
|
||||
<span v-else class="text-grey-darken-1">—</span>
|
||||
</td>
|
||||
<td>
|
||||
<BerthInfoSentStatusBadge
|
||||
v-if="item['Berth Info Sent Status']"
|
||||
:berthInfoSentStatus="item['Berth Info Sent Status']"
|
||||
/>
|
||||
<span v-else class="text-grey-darken-1">—</span>
|
||||
</td>
|
||||
<td>
|
||||
<ContractSentStatusBadge
|
||||
v-if="item['Contract Sent Status']"
|
||||
:contractSentStatus="item['Contract Sent Status']"
|
||||
/>
|
||||
<span v-else class="text-grey-darken-1">—</span>
|
||||
</td>
|
||||
<td>
|
||||
<Deposit10PercentStatusBadge
|
||||
v-if="item['Deposit 10% Status']"
|
||||
:deposit10PercentStatus="item['Deposit 10% Status']"
|
||||
/>
|
||||
<span v-else class="text-grey-darken-1">—</span>
|
||||
</td>
|
||||
<td>
|
||||
<ContractStatusBadge
|
||||
v-if="item['Contract Status']"
|
||||
|
|
@ -183,23 +175,6 @@
|
|||
<div class="text-grey-darken-1">{{ getRelativeTime(item["Created At"]) }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<v-tooltip v-if="item['Extra Comments']" location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
size="small"
|
||||
color="orange"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="small">mdi-comment-text</v-icon>
|
||||
Note
|
||||
</v-chip>
|
||||
</template>
|
||||
<span>{{ item["Extra Comments"] }}</span>
|
||||
</v-tooltip>
|
||||
<span v-else class="text-grey-darken-1"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
|
|
@ -230,9 +205,6 @@ import InterestSalesBadge from "~/components/InterestSalesBadge.vue";
|
|||
import InterestDetailsModal from "~/components/InterestDetailsModal.vue";
|
||||
import CreateInterestModal from "~/components/CreateInterestModal.vue";
|
||||
import EOIStatusBadge from "~/components/EOIStatusBadge.vue";
|
||||
import BerthInfoSentStatusBadge from "~/components/BerthInfoSentStatusBadge.vue";
|
||||
import ContractSentStatusBadge from "~/components/ContractSentStatusBadge.vue";
|
||||
import Deposit10PercentStatusBadge from "~/components/Deposit10PercentStatusBadge.vue";
|
||||
import ContractStatusBadge from "~/components/ContractStatusBadge.vue";
|
||||
import { useFetch } from "#app";
|
||||
import { ref, computed } from "vue";
|
||||
|
|
@ -285,19 +257,25 @@ const handleInterestCreated = async (interest: Interest) => {
|
|||
};
|
||||
|
||||
const headers = [
|
||||
{ title: "Contact", key: "Full Name", sortable: true, width: "20%" },
|
||||
{ title: "Berth", key: "Berth Number", sortable: true },
|
||||
{ title: "Contact", key: "Full Name", sortable: true, width: "25%" },
|
||||
{ title: "Sales Status", key: "Sales Process Level", sortable: true },
|
||||
{ title: "EOI Status", key: "EOI Status", sortable: true },
|
||||
{ title: "Berth Info", key: "Berth Info Sent Status", sortable: true },
|
||||
{ title: "Contract Sent", key: "Contract Sent Status", sortable: true },
|
||||
{ title: "Deposit 10%", key: "Deposit 10% Status", sortable: true },
|
||||
{ title: "Contract", key: "Contract Status", sortable: true },
|
||||
{ title: "Category", key: "Lead Category", sortable: true },
|
||||
{ title: "Created", key: "Created At", sortable: true },
|
||||
{ title: "", key: "Extra Comments", sortable: false },
|
||||
];
|
||||
|
||||
// Check if any filters are active
|
||||
const hasActiveFilters = computed(() => {
|
||||
return search.value !== '' || selectedSalesLevel.value !== 'all';
|
||||
});
|
||||
|
||||
// Clear all filters
|
||||
const clearAllFilters = () => {
|
||||
search.value = '';
|
||||
selectedSalesLevel.value = 'all';
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return "-";
|
||||
|
||||
|
|
@ -481,12 +459,86 @@ const getRelativeTime = (dateString: string) => {
|
|||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Mobile horizontal scrolling */
|
||||
/* Mobile horizontal scrolling with visual indicators */
|
||||
.table-container {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Scroll indicators */
|
||||
.table-container::before,
|
||||
.table-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 40px;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.table-container::before {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, white, transparent);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.table-container::after {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, white, transparent);
|
||||
}
|
||||
|
||||
.table-container:not(.scroll-start)::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.table-container:not(.scroll-end)::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Safari-specific fixes */
|
||||
.modern-table :deep(.v-table__wrapper) {
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.modern-table :deep(.v-data-table__td) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Column width constraints */
|
||||
.modern-table :deep(th:nth-child(1)),
|
||||
.modern-table :deep(td:nth-child(1)) {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.modern-table :deep(th:nth-child(2)),
|
||||
.modern-table :deep(td:nth-child(2)) {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.modern-table :deep(th:nth-child(3)),
|
||||
.modern-table :deep(td:nth-child(3)) {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.modern-table :deep(th:nth-child(4)),
|
||||
.modern-table :deep(td:nth-child(4)) {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.modern-table :deep(th:nth-child(5)),
|
||||
.modern-table :deep(td:nth-child(5)) {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.modern-table :deep(th:nth-child(6)),
|
||||
.modern-table :deep(td:nth-child(6)) {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.table-container {
|
||||
margin: 0 -12px;
|
||||
|
|
@ -494,7 +546,7 @@ const getRelativeTime = (dateString: string) => {
|
|||
}
|
||||
|
||||
.modern-table :deep(.v-table__wrapper) {
|
||||
min-width: 1200px;
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.modern-table :deep(th) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { deleteInterest } from "~/server/utils/nocodb";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
const { id } = body;
|
||||
const xTag = getHeader(event, "x-tag");
|
||||
|
||||
try {
|
||||
// Delete the interest from NocoDB
|
||||
await deleteInterest(id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Interest deleted successfully",
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || "Failed to delete interest",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -79,7 +79,7 @@ export const updateInterest = async (id: string, data: Partial<Interest>) => {
|
|||
// Filter the data to only include allowed fields
|
||||
for (const field of allowedFields) {
|
||||
if (field in data) {
|
||||
cleanData[field] = data[field];
|
||||
cleanData[field] = (data as any)[field];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ export const createInterest = async (data: Partial<Interest>) => {
|
|||
// Filter the data to only include allowed fields
|
||||
for (const field of allowedFields) {
|
||||
if (field in data) {
|
||||
cleanData[field] = data[field];
|
||||
cleanData[field] = (data as any)[field];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +146,14 @@ export const createInterest = async (data: Partial<Interest>) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const deleteInterest = async (id: string) =>
|
||||
$fetch(`${createTableUrl(Table.Interest)}/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"xc-token": getNocoDbConfiguration().token,
|
||||
},
|
||||
});
|
||||
|
||||
export const triggerWebhook = async (url: string, payload: any) =>
|
||||
$fetch(url, {
|
||||
method: "POST",
|
||||
|
|
|
|||
Loading…
Reference in New Issue