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:
Matt 2025-06-04 19:51:51 +02:00
parent 4d3935e863
commit 2ea72ef24e
5 changed files with 280 additions and 81 deletions

View File

@ -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();

View File

@ -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">

View File

@ -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) {

View File

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

View File

@ -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",