port-nimara-client-portal/pages/dashboard/interest-status.vue

493 lines
15 KiB
Vue

<template>
<v-container fluid class="pa-4">
<!-- Header -->
<v-row class="mb-4">
<v-col cols="12">
<h1 class="text-h4 font-weight-bold mb-2">
<v-icon class="mr-2" color="primary">mdi-view-column</v-icon>
Interest Status Board
</h1>
<p class="text-subtitle-1 text-grey-darken-1">
Track interests through the sales pipeline
</p>
</v-col>
</v-row>
<!-- Kanban Board -->
<div class="kanban-container">
<v-row class="flex-nowrap overflow-x-auto" style="min-height: calc(100vh - 200px);">
<v-col
v-for="(level, index) in InterestSalesProcessLevelFlow"
:key="level"
cols="auto"
class="kanban-column"
v-show="groupedInterests[level].length > 0"
>
<v-card
class="h-100 d-flex flex-column column-card"
:style="{ minWidth: '340px', maxWidth: '340px' }"
elevation="0"
>
<!-- Column Header -->
<div class="column-header pa-3">
<v-chip
:color="getColumnColor(level)"
class="text-white font-weight-medium"
label
>
<v-icon start>{{ getColumnIcon(level) }}</v-icon>
{{ level }}
<v-chip
size="small"
variant="flat"
color="white"
class="ml-2"
style="background: rgba(255,255,255,0.3) !important;"
>
{{ groupedInterests[level].length }}
</v-chip>
</v-chip>
</div>
<!-- Column Content -->
<v-card-text
class="flex-grow-1 overflow-y-auto pa-3 column-content"
@dragover="handleDragOver($event)"
@drop="handleDrop($event, level)"
@dragleave="handleDragLeave($event)"
>
<v-card
v-for="interest in groupedInterests[level]"
:key="interest.Id"
class="mb-3 interest-card"
:class="{ 'dragging': draggedInterest?.Id === interest.Id }"
elevation="1"
draggable="true"
@dragstart="handleDragStart($event, interest)"
@dragend="handleDragEnd($event)"
@click="handleInterestClick(interest.Id)"
>
<v-card-text class="pa-3">
<!-- Name and Yacht -->
<div class="d-flex align-center mb-2">
<v-avatar size="32" :color="getColumnColor(level)" class="mr-2">
<span class="text-white text-caption font-weight-bold">
{{ getInitials(interest["Full Name"]) }}
</span>
</v-avatar>
<div class="flex-grow-1">
<div class="font-weight-medium">{{ interest["Full Name"] }}</div>
<div class="text-caption text-grey-darken-1">{{ interest["Yacht Name"] || "No yacht specified" }}</div>
</div>
</div>
<!-- Contact Info -->
<div class="mb-2">
<div class="d-flex align-center mb-1" v-if="interest['Email Address']">
<v-icon size="small" class="mr-2" color="grey-darken-2">mdi-email</v-icon>
<span class="text-caption text-truncate">{{ interest["Email Address"] }}</span>
</div>
<div class="d-flex align-center mb-1" v-if="interest['Phone Number']">
<v-icon size="small" class="mr-2" color="grey-darken-2">mdi-phone</v-icon>
<span class="text-caption">{{ interest["Phone Number"] }}</span>
</div>
</div>
<!-- Berth Info -->
<div class="d-flex flex-wrap ga-1">
<v-chip
v-if="interest['Berth Number']"
size="x-small"
color="deep-purple"
variant="tonal"
>
<v-icon start size="x-small">mdi-anchor</v-icon>
{{ interest["Berth Number"] }}
</v-chip>
<v-chip
v-if="interest['Berth Size Desired']"
size="x-small"
color="blue"
variant="tonal"
>
<v-icon start size="x-small">mdi-ruler</v-icon>
{{ interest["Berth Size Desired"] }}
</v-chip>
</div>
<!-- Extra Comments -->
<div v-if="interest['Extra Comments']" class="mt-2">
<v-chip size="x-small" color="orange" variant="tonal">
<v-icon start size="x-small">mdi-comment</v-icon>
Has notes
</v-chip>
</div>
</v-card-text>
</v-card>
</v-card-text>
</v-card>
</v-col>
<!-- Empty state message if no columns are visible -->
<v-col
v-if="visibleColumnsCount === 0"
cols="12"
class="text-center py-8"
>
<v-icon size="64" color="grey-lighten-1">mdi-view-column-outline</v-icon>
<h3 class="text-h5 mt-4 mb-2">No interests to display</h3>
<p class="text-grey-darken-1">Interests will appear here as they progress through the sales pipeline</p>
</v-col>
</v-row>
</div>
</v-container>
<!-- Interest Details Modal -->
<InterestDetailsModal
v-model="showModal"
:selected-interest="selectedInterest"
@save="handleSaveInterest"
@request-more-info-to-sales="handleRequestMoreInfoToSales"
@request-more-information="handleRequestMoreInformation"
@eoi-send-to-sales="handleEoiSendToSales"
/>
</template>
<script lang="ts" setup>
import InterestDetailsModal from "~/components/InterestDetailsModal.vue";
import InterestSalesBadge from "~/components/InterestSalesBadge.vue";
import { useFetch } from "#app";
import { ref, computed } from "vue";
import type { Interest, InterestSalesProcessLevel, InterestsResponse } from "@/utils/types";
import { InterestSalesProcessLevelFlow } from "@/utils/types";
useHead({
title: "Interest Status",
});
const user = useDirectusUser();
const loading = ref(true);
const showModal = ref(false);
const selectedInterest = ref<Interest | null>(null);
const draggedInterest = ref<Interest | null>(null);
const draggedFromLevel = ref<string | null>(null);
const { data: interests } = useFetch<InterestsResponse>("/api/get-interests", {
headers: {
"x-tag": user.value?.email ? "094ut234" : "pjnvü1230",
},
onResponse() {
loading.value = false;
},
onResponseError() {
loading.value = false;
},
});
const groupedInterests = computed(() => {
const groups: Record<string, Interest[]> = {};
InterestSalesProcessLevelFlow.forEach((level) => {
groups[level] = [];
});
interests.value?.list.forEach((interest) => {
const level = interest["Sales Process Level"];
if (groups[level]) {
groups[level].push(interest);
}
});
// Sort each group by "Created At" (newest first)
for (const level in groups) {
groups[level].sort((a, b) => {
// Convert date strings to comparable format
const dateA = parseDate(a["Created At"]);
const dateB = parseDate(b["Created At"]);
return dateB.getTime() - dateA.getTime();
});
}
return groups;
});
// Count visible columns
const visibleColumnsCount = computed(() => {
return InterestSalesProcessLevelFlow.filter(level => groupedInterests.value[level].length > 0).length;
});
// Helper function to parse date
const parseDate = (dateString: string) => {
if (!dateString) return new Date(0);
// Handle DD-MM-YYYY format
if (dateString.includes('-')) {
const parts = dateString.split('-');
if (parts.length === 3) {
const [day, month, year] = parts;
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
}
}
return new Date(dateString);
};
// Helper function to get initials
const getInitials = (name: string) => {
if (!name) return '?';
const parts = name.split(' ');
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
return name.substring(0, 2).toUpperCase();
};
// Get column color based on sales process level
const getColumnColor = (level: string) => {
const colorMap: Record<string, string> = {
'General Qualified Interest': 'grey-darken-1',
'Specific Qualified Interest': 'grey-darken-2',
'LOI and NDA Sent': 'light-blue',
'Signed LOI and NDA': 'blue',
'Made Reservation': 'green',
'Contract Negotiation': 'orange',
'Contract Negotiations Finalized': 'deep-orange',
'Contract Signed': 'green-darken-2'
};
return colorMap[level] || 'grey';
};
// Get column icon based on sales process level
const getColumnIcon = (level: string) => {
const iconMap: Record<string, string> = {
'General Qualified Interest': 'mdi-account-search',
'Specific Qualified Interest': 'mdi-account-check',
'LOI and NDA Sent': 'mdi-email-send',
'Signed LOI and NDA': 'mdi-file-sign',
'Made Reservation': 'mdi-calendar-check',
'Contract Negotiation': 'mdi-handshake',
'Contract Negotiations Finalized': 'mdi-file-document-check',
'Contract Signed': 'mdi-check-circle'
};
return iconMap[level] || 'mdi-circle';
};
const handleInterestClick = (interestId: number) => {
// Find the interest by ID
const interest = interests.value?.list.find(i => i.Id === interestId);
if (interest) {
selectedInterest.value = { ...interest };
showModal.value = true;
}
};
// Event handlers for the modal
const handleSaveInterest = (interest: Interest) => {
// Update the interest in the local list if needed
// The modal component already handles the API call and shows success/error messages
// You can add additional logic here if needed
};
const handleRequestMoreInfoToSales = (interest: Interest) => {
// The modal component already handles the API call
// You can add additional logic here if needed
};
const handleRequestMoreInformation = (interest: Interest) => {
// The modal component already handles the API call
// You can add additional logic here if needed
};
const handleEoiSendToSales = (interest: Interest) => {
// The modal component already handles the API call
// You can add additional logic here if needed
};
// Drag and Drop handlers
const handleDragStart = (event: DragEvent, interest: Interest) => {
draggedInterest.value = interest;
draggedFromLevel.value = interest["Sales Process Level"];
// Add visual feedback
if (event.target instanceof HTMLElement) {
event.target.style.opacity = '0.5';
}
// Store interest data in dataTransfer
event.dataTransfer!.effectAllowed = 'move';
event.dataTransfer!.setData('text/plain', JSON.stringify(interest));
};
const handleDragEnd = (event: DragEvent) => {
// Remove visual feedback
if (event.target instanceof HTMLElement) {
event.target.style.opacity = '';
}
draggedInterest.value = null;
draggedFromLevel.value = null;
};
const handleDragOver = (event: DragEvent) => {
event.preventDefault();
event.dataTransfer!.dropEffect = 'move';
// Add visual feedback to drop zone
if (event.currentTarget instanceof HTMLElement) {
event.currentTarget.classList.add('drag-over');
}
};
const handleDragLeave = (event: DragEvent) => {
// Remove visual feedback from drop zone
if (event.currentTarget instanceof HTMLElement) {
event.currentTarget.classList.remove('drag-over');
}
};
const handleDrop = async (event: DragEvent, targetLevel: string) => {
event.preventDefault();
// Remove visual feedback
if (event.currentTarget instanceof HTMLElement) {
event.currentTarget.classList.remove('drag-over');
}
if (!draggedInterest.value || draggedFromLevel.value === targetLevel) {
return;
}
try {
// Update the interest's sales process level
const response = await $fetch('/api/update-interest', {
method: 'POST',
headers: {
'x-tag': '094ut234'
},
body: {
id: draggedInterest.value.Id,
data: {
'Sales Process Level': targetLevel
}
}
});
// Update local data
const interestIndex = interests.value?.list.findIndex(i => i.Id === draggedInterest.value!.Id);
if (interestIndex !== undefined && interestIndex !== -1 && interests.value) {
interests.value.list[interestIndex]['Sales Process Level'] = targetLevel;
}
// Show success message
const { $toast } = useNuxtApp();
$toast.success(`Moved to ${targetLevel}`);
} catch (error) {
console.error('Failed to update interest:', error);
const { $toast } = useNuxtApp();
$toast.error('Failed to update interest status');
}
};
</script>
<style scoped>
.kanban-container {
background-color: #f5f5f5;
border-radius: 12px;
padding: 20px;
}
.kanban-column {
padding: 0 10px;
}
.column-card {
background: #fafafa;
border: 1px solid #e0e0e0;
}
.column-header {
background: white;
border-bottom: 1px solid #e0e0e0;
}
.column-content {
background: transparent;
position: relative;
}
.column-content.drag-over {
background: rgba(33, 150, 243, 0.1);
border: 2px dashed #2196F3;
border-radius: 4px;
}
.interest-card {
cursor: move;
transition: all 0.2s ease;
background: white;
border: 1px solid #e0e0e0;
}
.interest-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
border-color: #d0d0d0;
}
.interest-card.dragging {
opacity: 0.5;
cursor: grabbing;
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 240px;
}
/* Custom scrollbar for column content */
.v-card-text::-webkit-scrollbar {
width: 6px;
}
.v-card-text::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
.v-card-text::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.v-card-text::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
/* Ensure horizontal scroll for the board */
.overflow-x-auto {
overflow-x: auto;
overflow-y: hidden;
}
.overflow-x-auto::-webkit-scrollbar {
height: 10px;
}
.overflow-x-auto::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
.overflow-x-auto::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
}
.overflow-x-auto::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>