updates
This commit is contained in:
parent
49aa47ab10
commit
839b307edd
|
|
@ -74,9 +74,123 @@
|
|||
</div>
|
||||
|
||||
<!-- Email Thread List -->
|
||||
<div v-if="emailThreads.length > 0" class="email-threads">
|
||||
<div class="text-subtitle-1 mb-3">Email History</div>
|
||||
<v-timeline :side="mobile ? 'end' : 'end'" :density="mobile ? 'compact' : 'comfortable'">
|
||||
<div v-if="emailThreads.length > 0 || threads.length > 0" class="email-threads">
|
||||
<div class="text-subtitle-1 mb-3 d-flex align-center">
|
||||
Email History
|
||||
<v-spacer />
|
||||
<v-btn-toggle
|
||||
v-model="viewMode"
|
||||
mandatory
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
<v-btn value="threads" size="small">
|
||||
<v-icon :start="!mobile">mdi-forum</v-icon>
|
||||
<span v-if="!mobile" class="ml-1">Threads</span>
|
||||
</v-btn>
|
||||
<v-btn value="all" size="small">
|
||||
<v-icon :start="!mobile">mdi-email-multiple</v-icon>
|
||||
<span v-if="!mobile" class="ml-1">All</span>
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<!-- Thread View -->
|
||||
<div v-if="viewMode === 'threads' && threads.length > 0">
|
||||
<v-expansion-panels v-model="expandedThreads" multiple>
|
||||
<v-expansion-panel v-for="thread in threads" :key="thread.id">
|
||||
<v-expansion-panel-title>
|
||||
<div class="d-flex align-center justify-space-between w-100">
|
||||
<div>
|
||||
<div class="text-body-2 font-weight-medium">{{ thread.subject }}</div>
|
||||
<div class="text-caption text-grey">
|
||||
{{ thread.emailCount }} {{ thread.emailCount === 1 ? 'email' : 'emails' }} •
|
||||
Last activity {{ formatRelativeTime(thread.latestTimestamp) }}
|
||||
</div>
|
||||
</div>
|
||||
<v-chip size="small" color="primary" variant="tonal">
|
||||
{{ thread.emailCount }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-timeline :density="mobile ? 'compact' : 'comfortable'" side="end">
|
||||
<v-timeline-item
|
||||
v-for="(email, emailIndex) in thread.emails"
|
||||
:key="emailIndex"
|
||||
:dot-color="email.direction === 'sent' ? 'primary' : 'success'"
|
||||
:icon="email.direction === 'sent' ? 'mdi-email-send' : 'mdi-email-receive'"
|
||||
:size="mobile ? 'x-small' : 'small'"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
:density="mobile ? 'compact' : 'default'"
|
||||
@click="viewEmail(email)"
|
||||
class="email-card"
|
||||
:hover="true"
|
||||
>
|
||||
<v-card-subtitle class="d-flex align-center justify-space-between">
|
||||
<span class="text-body-2">
|
||||
{{ email.direction === 'sent' ? 'To' : 'From' }}:
|
||||
{{ email.direction === 'sent' ? email.to : email.from }}
|
||||
</span>
|
||||
<span class="text-caption text-grey">
|
||||
{{ formatDate(email.timestamp) }}
|
||||
</span>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text :class="mobile ? 'pa-3' : ''">
|
||||
<div class="email-content email-preview" :class="mobile ? 'email-content-mobile' : ''" v-html="formatEmailContent(email.content || email.body)"></div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div v-if="email.attachments && email.attachments.length > 0" class="mt-3">
|
||||
<v-chip
|
||||
v-for="(attachment, i) in email.attachments"
|
||||
:key="i"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-paperclip"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ attachment.name || attachment }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-space-between mt-3">
|
||||
<v-btn variant="text" size="small" color="primary" @click.stop="viewEmail(email)">
|
||||
<v-icon start>mdi-email-open</v-icon>
|
||||
View
|
||||
</v-btn>
|
||||
<v-btn variant="text" size="small" color="primary" @click.stop="replyToEmail(email)">
|
||||
<v-icon start>mdi-reply</v-icon>
|
||||
Reply
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
|
||||
<!-- Reply to thread button -->
|
||||
<div class="text-center mt-3">
|
||||
<v-btn
|
||||
@click="replyToThread(thread)"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-reply"
|
||||
>
|
||||
Reply to Thread
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</div>
|
||||
|
||||
<!-- All Emails View (Original) -->
|
||||
<v-timeline v-else :side="mobile ? 'end' : 'end'" :density="mobile ? 'compact' : 'comfortable'">
|
||||
<v-timeline-item
|
||||
v-for="(email, index) in emailThreads"
|
||||
:key="index"
|
||||
|
|
@ -90,7 +204,13 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<v-card variant="outlined" :density="mobile ? 'compact' : 'default'">
|
||||
<v-card
|
||||
variant="outlined"
|
||||
:density="mobile ? 'compact' : 'default'"
|
||||
@click="viewEmail(email)"
|
||||
class="email-card"
|
||||
:hover="true"
|
||||
>
|
||||
<v-card-subtitle class="d-flex align-center justify-space-between">
|
||||
<span class="text-body-2">
|
||||
{{ email.direction === 'sent' ? 'To' : 'From' }}:
|
||||
|
|
@ -103,7 +223,7 @@
|
|||
|
||||
<v-card-text :class="mobile ? 'pa-3' : ''">
|
||||
<div class="text-body-2 font-weight-medium mb-2">{{ email.subject }}</div>
|
||||
<div class="email-content" :class="mobile ? 'email-content-mobile' : ''" v-html="formatEmailContent(email.content)"></div>
|
||||
<div class="email-content email-preview" :class="mobile ? 'email-content-mobile' : ''" v-html="formatEmailContent(email.content || email.body)"></div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div v-if="email.attachments && email.attachments.length > 0" class="mt-3">
|
||||
|
|
@ -116,9 +236,20 @@
|
|||
prepend-icon="mdi-paperclip"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ attachment.name }}
|
||||
{{ attachment.name || attachment }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-space-between mt-3">
|
||||
<v-btn variant="text" size="small" color="primary">
|
||||
<v-icon start>mdi-email-open</v-icon>
|
||||
View Full Email
|
||||
</v-btn>
|
||||
<v-btn variant="text" size="small" color="primary" @click.stop="replyToEmail(email)">
|
||||
<v-icon start>mdi-reply</v-icon>
|
||||
Reply
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-timeline-item>
|
||||
|
|
@ -392,12 +523,19 @@
|
|||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Email Details Dialog -->
|
||||
<EmailDetailsDialog
|
||||
v-model="showEmailDetails"
|
||||
:email="selectedEmail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Interest } from '~/utils/types';
|
||||
import FileBrowser from '~/pages/dashboard/file-browser.vue';
|
||||
import EmailDetailsDialog from '~/components/EmailDetailsDialog.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
interest: Interest;
|
||||
|
|
@ -423,6 +561,12 @@ const includeSignature = ref(true);
|
|||
const signatureConfig = ref<any>({});
|
||||
const showFileBrowser = ref(false);
|
||||
const tempSelectedFiles = ref<any[]>([]);
|
||||
const showEmailDetails = ref(false);
|
||||
const selectedEmail = ref<any>(null);
|
||||
const threads = ref<any[]>([]);
|
||||
const viewMode = ref('threads');
|
||||
const expandedThreads = ref<number[]>([0]); // Expand first thread by default
|
||||
const replyingTo = ref<any>(null);
|
||||
|
||||
const emailDraft = ref<{
|
||||
subject: string;
|
||||
|
|
@ -530,7 +674,7 @@ const loadEmailThread = async () => {
|
|||
// Check if we have threads from the API response
|
||||
if (response.threads) {
|
||||
console.log('[ClientEmailSection] Threads available:', response.threads.length);
|
||||
// For now, still use emails until we implement thread UI
|
||||
threads.value = response.threads;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -747,6 +891,7 @@ const closeComposer = () => {
|
|||
};
|
||||
attachmentMode.value = 'upload';
|
||||
selectedBrowserFiles.value = [];
|
||||
replyingTo.value = null;
|
||||
};
|
||||
|
||||
const removeBrowserFile = (index: number) => {
|
||||
|
|
@ -840,15 +985,86 @@ const cancelFileBrowser = () => {
|
|||
showFileBrowser.value = false;
|
||||
tempSelectedFiles.value = [];
|
||||
};
|
||||
|
||||
const viewEmail = (email: any) => {
|
||||
selectedEmail.value = email;
|
||||
showEmailDetails.value = true;
|
||||
};
|
||||
|
||||
const replyToEmail = (email: any) => {
|
||||
replyingTo.value = email;
|
||||
|
||||
// Pre-fill the subject with Re: prefix if not already there
|
||||
const subject = email.subject || '';
|
||||
emailDraft.value.subject = subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
||||
|
||||
// Pre-fill with a reply header
|
||||
const originalDate = formatDate(email.timestamp);
|
||||
const originalFrom = email.direction === 'sent' ? 'you' : email.from;
|
||||
|
||||
emailDraft.value.content = `\n\n\n--- Original Message ---\nFrom: ${originalFrom}\nDate: ${originalDate}\n\n${email.content || email.body || ''}`;
|
||||
|
||||
showComposer.value = true;
|
||||
};
|
||||
|
||||
const replyToThread = (thread: any) => {
|
||||
// Reply to the last email in the thread
|
||||
const lastEmail = thread.emails[thread.emails.length - 1];
|
||||
replyToEmail(lastEmail);
|
||||
};
|
||||
|
||||
const formatRelativeTime = (timestamp: string) => {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return 'just now';
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
|
||||
return date.toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.email-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.email-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.email-content {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.email-preview {
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.email-preview::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 30px;
|
||||
background: linear-gradient(to bottom, transparent, white);
|
||||
}
|
||||
|
||||
.email-content-mobile {
|
||||
max-height: 150px;
|
||||
font-size: 0.875rem;
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
:preferred-countries="['US', 'FR', 'ES', 'PT', 'GB']"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="newInterest.Address"
|
||||
label="Address"
|
||||
|
|
@ -94,16 +94,7 @@
|
|||
prepend-inner-icon="mdi-map-marker"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<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-col cols="12" md="4">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="newInterest['Contact Method Preferred']"
|
||||
label="Contact Method Preferred"
|
||||
|
|
@ -353,7 +344,6 @@ const getInitialInterest = () => ({
|
|||
"Phone Number": "",
|
||||
"Contact Method Preferred": "Email",
|
||||
Address: "",
|
||||
"Place of Residence": "",
|
||||
"Yacht Name": "",
|
||||
"Berth Size Desired": "",
|
||||
Length: "",
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@
|
|||
:loading="isGenerating"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
:prepend-icon="!mobile ? 'mdi-file-document-plus' : undefined"
|
||||
:icon="mobile ? 'mdi-file-document-plus' : undefined"
|
||||
prepend-icon="mdi-file-document-plus"
|
||||
:size="mobile ? 'default' : 'default'"
|
||||
:block="mobile"
|
||||
>
|
||||
<span v-if="!mobile">Generate EOI</span>
|
||||
Generate EOI
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<v-dialog
|
||||
v-model="isOpen"
|
||||
:max-width="mobile ? '100%' : '800'"
|
||||
:fullscreen="mobile"
|
||||
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
||||
>
|
||||
<v-card v-if="email">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-2" :color="email.direction === 'sent' ? 'primary' : 'success'">
|
||||
{{ email.direction === 'sent' ? 'mdi-email-send' : 'mdi-email-receive' }}
|
||||
</v-icon>
|
||||
Email Details
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="close"></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text>
|
||||
<v-list density="comfortable">
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ email.direction === 'sent' ? 'To' : 'From' }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ email.direction === 'sent' ? email.to : email.from }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-text-box</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Subject</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ email.subject }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-calendar-clock</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Date & Time</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(email.timestamp) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="email-body">
|
||||
<div class="text-subtitle-2 mb-2">Message</div>
|
||||
<div class="email-content" v-html="formatEmailContent(email.content || email.body)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments -->
|
||||
<div v-if="email.attachments && email.attachments.length > 0" class="mt-4">
|
||||
<div class="text-subtitle-2 mb-2">Attachments ({{ email.attachments.length }})</div>
|
||||
<v-chip
|
||||
v-for="(attachment, i) in email.attachments"
|
||||
:key="i"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-paperclip"
|
||||
class="mr-2 mb-2"
|
||||
>
|
||||
{{ attachment.name || attachment }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="close" variant="text">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
email: any;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const { mobile } = useDisplay();
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '';
|
||||
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
};
|
||||
|
||||
const formatEmailContent = (content: string) => {
|
||||
if (!content) return '';
|
||||
|
||||
// If it's already HTML, return as is
|
||||
if (content.includes('<p>') || content.includes('<br>')) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Convert plain text to HTML
|
||||
return content
|
||||
.split('\n')
|
||||
.map(line => line.trim() ? `<p>${line}</p>` : '<br>')
|
||||
.join('');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.email-content {
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.email-content :deep(p) {
|
||||
margin: 0 0 0.5em 0;
|
||||
}
|
||||
|
||||
.email-content :deep(br) {
|
||||
display: block;
|
||||
content: "";
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1222,24 +1222,22 @@ const getSalesLevelColor = (level: string) => {
|
|||
|
||||
// Confirm delete
|
||||
const confirmDelete = () => {
|
||||
if (!interest.value) return;
|
||||
if (!interest.value || isDeleting.value) return;
|
||||
|
||||
if (confirm(`Are you sure you want to delete the interest for ${interest.value['Full Name']}? This action cannot be undone.`)) {
|
||||
if (debouncedDeleteInterest) {
|
||||
debouncedDeleteInterest();
|
||||
} else {
|
||||
deleteInterest();
|
||||
}
|
||||
deleteInterest();
|
||||
}
|
||||
};
|
||||
|
||||
// Delete interest
|
||||
const deleteInterest = async () => {
|
||||
if (!interest.value) return;
|
||||
if (!interest.value || isDeleting.value) return;
|
||||
|
||||
console.log('[InterestDetailsModal] Starting delete for interest:', interest.value.Id);
|
||||
isDeleting.value = true;
|
||||
|
||||
try {
|
||||
await $fetch("/api/delete-interest", {
|
||||
const response = await $fetch("/api/delete-interest", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-tag": user.value?.email ? "094ut234" : "pjnvü1230",
|
||||
|
|
@ -1249,12 +1247,15 @@ const deleteInterest = async () => {
|
|||
},
|
||||
});
|
||||
|
||||
console.log('[InterestDetailsModal] Delete response:', response);
|
||||
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.");
|
||||
} catch (error: any) {
|
||||
console.error("[InterestDetailsModal] Failed to delete interest:", error);
|
||||
console.error("[InterestDetailsModal] Error details:", error.data || error.message);
|
||||
const errorMessage = error.data?.statusMessage || error.message || "Failed to delete interest. Please try again.";
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -585,37 +585,51 @@ const getRelativeTime = (dateString: string) => {
|
|||
/* Mobile-specific styles */
|
||||
@media (max-width: 768px) {
|
||||
.table-container {
|
||||
margin: 0 -12px;
|
||||
padding: 0 12px;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -16px;
|
||||
}
|
||||
|
||||
/* For mobile, only show essential columns */
|
||||
/* Add padding to the wrapper instead */
|
||||
.modern-table :deep(.v-table__wrapper) {
|
||||
min-width: auto;
|
||||
padding: 0 16px;
|
||||
min-width: 600px; /* Minimum width to ensure scrolling */
|
||||
}
|
||||
|
||||
.modern-table :deep(th) {
|
||||
padding: 8px !important;
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modern-table :deep(td) {
|
||||
padding: 12px 8px !important;
|
||||
}
|
||||
|
||||
/* Hide columns on mobile that aren't in mobile headers */
|
||||
.modern-table :deep(th:nth-child(n+4)),
|
||||
.modern-table :deep(td:nth-child(n+4)) {
|
||||
display: none;
|
||||
/* Show all columns but with smaller widths on mobile */
|
||||
.modern-table :deep(th:nth-child(1)),
|
||||
.modern-table :deep(td:nth-child(1)) {
|
||||
min-width: 180px !important;
|
||||
}
|
||||
|
||||
.modern-table :deep(th:nth-child(2)),
|
||||
.modern-table :deep(td:nth-child(2)) {
|
||||
min-width: 120px !important;
|
||||
}
|
||||
|
||||
.modern-table :deep(th:nth-child(3)),
|
||||
.modern-table :deep(td:nth-child(3)) {
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
/* Contact cell optimization */
|
||||
.contact-cell {
|
||||
max-width: 200px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.contact-cell .text-truncate {
|
||||
max-width: 150px;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
/* Adjust table row height on mobile */
|
||||
|
|
@ -628,6 +642,18 @@ const getRelativeTime = (dateString: string) => {
|
|||
height: 20px !important;
|
||||
font-size: 0.625rem !important;
|
||||
}
|
||||
|
||||
/* Add visual scroll indicators */
|
||||
.table-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
background: linear-gradient(to right, transparent, rgba(255,255,255,0.8));
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure proper text truncation */
|
||||
|
|
|
|||
|
|
@ -107,7 +107,16 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
// Update interest with EOI document information
|
||||
console.log('[EOI Upload] Updating interest with EOI document info');
|
||||
await updateInterestEOIDocument(interestId, documentData);
|
||||
console.log('[EOI Upload] Document data:', JSON.stringify(documentData, null, 2));
|
||||
|
||||
try {
|
||||
await updateInterestEOIDocument(interestId, documentData);
|
||||
console.log('[EOI Upload] Successfully updated EOI document in database');
|
||||
} catch (dbError: any) {
|
||||
console.error('[EOI Upload] Failed to update database with EOI document:', dbError);
|
||||
console.error('[EOI Upload] Database error details:', dbError.data || dbError.message);
|
||||
throw new Error(`Failed to update database: ${dbError.message}`);
|
||||
}
|
||||
|
||||
// Update the status fields for uploaded (signed) EOI
|
||||
const updateData: any = {
|
||||
|
|
@ -116,18 +125,26 @@ export default defineEventHandler(async (event) => {
|
|||
'Sales Process Level': 'Signed LOI and NDA'
|
||||
};
|
||||
console.log('[EOI Upload] Updating interest status fields for signed EOI');
|
||||
console.log('[EOI Upload] Status update data:', JSON.stringify(updateData, null, 2));
|
||||
|
||||
// Update the interest
|
||||
await $fetch('/api/update-interest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-tag': xTagHeader,
|
||||
},
|
||||
body: {
|
||||
id: interestId,
|
||||
data: updateData
|
||||
}
|
||||
});
|
||||
try {
|
||||
// Update the interest
|
||||
await $fetch('/api/update-interest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-tag': xTagHeader,
|
||||
},
|
||||
body: {
|
||||
id: interestId,
|
||||
data: updateData
|
||||
}
|
||||
});
|
||||
console.log('[EOI Upload] Successfully updated interest status');
|
||||
} catch (statusError: any) {
|
||||
console.error('[EOI Upload] Failed to update interest status:', statusError);
|
||||
console.error('[EOI Upload] Status error details:', statusError.data || statusError.message);
|
||||
// Don't throw here - the file was uploaded successfully
|
||||
}
|
||||
|
||||
console.log('[EOI Upload] Upload completed successfully');
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,20 @@ import { promises as fs } from 'fs';
|
|||
import mime from 'mime-types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const xTagHeader = getRequestHeader(event, "x-tag");
|
||||
|
||||
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
||||
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the current path and bucket from query params
|
||||
const query = getQuery(event);
|
||||
const currentPath = (query.path as string) || '';
|
||||
const bucket = (query.bucket as string) || 'client-portal'; // Default bucket
|
||||
|
||||
console.log('[Upload] Request received for bucket:', bucket, 'path:', currentPath);
|
||||
|
||||
// Parse multipart form data
|
||||
const form = formidable({
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB limit
|
||||
|
|
@ -50,6 +58,15 @@ export default defineEventHandler(async (event) => {
|
|||
} else {
|
||||
// For other buckets, use the MinIO client directly
|
||||
const client = getMinioClient();
|
||||
|
||||
// Ensure bucket exists
|
||||
try {
|
||||
await client.bucketExists(bucket);
|
||||
} catch (err) {
|
||||
console.log(`[Upload] Bucket ${bucket} doesn't exist, creating it...`);
|
||||
await client.makeBucket(bucket, 'us-east-1');
|
||||
}
|
||||
|
||||
await client.putObject(bucket, fullPath, fileBuffer, fileBuffer.length, {
|
||||
'Content-Type': contentType,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue