This commit is contained in:
Matt 2025-06-10 12:54:22 +02:00
parent 5e4b20f6ae
commit 5c30411c2b
5 changed files with 241 additions and 59 deletions

View File

@ -4,7 +4,7 @@
<v-icon class="mr-2" color="primary">mdi-file-document-edit</v-icon>
EOI Management
</v-card-title>
<v-card-text class="pt-2">
<v-card-text class="pt-0">
<!-- Generate EOI Button -->
<div v-if="!hasEOI" class="mb-4">

View File

@ -171,25 +171,40 @@
</v-card>
<!-- File Browser Dialog -->
<v-dialog v-model="showFileBrowser" max-width="800">
<v-dialog v-model="showFileBrowser" max-width="900" :fullscreen="mobile">
<v-card>
<v-card-title>
Select Files to Attach
<v-toolbar color="primary" dark>
<v-toolbar-title>
<v-icon class="mr-2">mdi-attachment</v-icon>
Select Files to Attach
</v-toolbar-title>
<v-spacer />
<v-btn icon @click="showFileBrowser = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text style="height: 500px; overflow-y: auto;">
<file-browser-component
v-if="showFileBrowser"
:selection-mode="true"
@file-selected="onFileSelected"
/>
</v-toolbar>
<v-card-text class="pa-0">
<div style="height: 600px; overflow: hidden;">
<file-browser-component
v-if="showFileBrowser"
:selection-mode="true"
@file-selected="onFileSelected"
/>
</div>
</v-card-text>
<v-card-actions>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn variant="text" @click="showFileBrowser = false">Cancel</v-btn>
<v-btn
variant="text"
size="large"
@click="showFileBrowser = false"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@ -220,6 +235,7 @@ const emit = defineEmits<Emits>();
const user = useDirectusUser();
const toast = useToast();
const { mobile } = useDisplay();
const form = ref();
const sending = ref(false);

View File

@ -66,7 +66,7 @@
variant="flat"
color="success"
size="large"
@click="saveInterest"
@click="() => debouncedSaveInterest ? debouncedSaveInterest() : saveInterest()"
:loading="isSaving"
:disabled="isSaving || isDeleting"
class="ml-2"
@ -195,7 +195,7 @@
</v-col>
<v-col cols="12">
<v-btn
@click="saveInterest"
@click="() => debouncedSaveInterest ? debouncedSaveInterest() : saveInterest()"
variant="flat"
color="success"
block
@ -258,15 +258,6 @@
prepend-inner-icon="mdi-map-marker"
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="interest['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-select
v-model="interest['Contact Method Preferred']"
@ -418,9 +409,9 @@
<v-col cols="12" md="6">
<v-autocomplete
v-model="selectedBerthRecommendations"
:items="availableBerths"
:item-title="(item) => item['Mooring Number']"
:item-value="(item) => item.Id"
:items="groupedBerths"
:item-title="(item) => item.isDivider ? '' : item['Mooring Number']"
:item-value="(item) => item.isDivider ? null : item.Id"
label="Berth Recommendations"
variant="outlined"
density="comfortable"
@ -431,6 +422,20 @@
prepend-inner-icon="mdi-star"
@update:model-value="updateBerthRecommendations"
>
<template v-slot:item="{ props, item }">
<v-divider v-if="item.raw.isDivider" class="mt-2 mb-2">
<template v-slot:default>
<div class="text-caption text-medium-emphasis px-2">
{{ item.raw.letter }}
</div>
</template>
</v-divider>
<v-list-item
v-else
v-bind="props"
:title="item.raw['Mooring Number']"
/>
</template>
<template v-slot:chip="{ props, item }">
<v-chip
v-bind="props"
@ -443,9 +448,9 @@
<v-col cols="12" md="6">
<v-autocomplete
v-model="selectedBerths"
:items="availableBerths"
:item-title="(item) => item['Mooring Number']"
:item-value="(item) => item.Id"
:items="groupedBerths"
:item-title="(item) => item.isDivider ? '' : item['Mooring Number']"
:item-value="(item) => item.isDivider ? null : item.Id"
label="Berths"
variant="outlined"
density="comfortable"
@ -456,6 +461,20 @@
prepend-inner-icon="mdi-dock-window"
@update:model-value="updateBerths"
>
<template v-slot:item="{ props, item }">
<v-divider v-if="item.raw.isDivider" class="mt-2 mb-2">
<template v-slot:default>
<div class="text-caption text-medium-emphasis px-2">
{{ item.raw.letter }}
</div>
</template>
</v-divider>
<v-list-item
v-else
v-bind="props"
:title="item.raw['Mooring Number']"
/>
</template>
<template v-slot:chip="{ props, item }">
<v-chip
v-bind="props"
@ -695,13 +714,34 @@ const selectedBerthRecommendations = ref<number[]>([]);
const originalBerths = ref<number[]>([]);
const originalBerthRecommendations = ref<number[]>([]);
// Auto-save function (debounced)
const autoSave = debounce(async () => {
if (!hasUnsavedChanges.value || !interest.value) return;
console.log('Auto-saving interest...');
await saveInterest(true); // Pass true to indicate auto-save
}, 2000); // 2 second delay
// Store the debounced functions
let autoSave: any = null;
let debouncedSaveInterest: any = null;
let debouncedDeleteInterest: any = null;
// Initialize debounced functions
const initializeDebouncedFunctions = () => {
// Auto-save function (debounced)
autoSave = debounce(async () => {
if (!hasUnsavedChanges.value || !interest.value || isSaving.value) return;
console.log('Auto-saving interest...');
await saveInterest(true); // Pass true to indicate auto-save
}, 2000); // 2 second delay
// Debounced manual save
debouncedSaveInterest = debounce(async () => {
await saveInterest();
}, 300); // 300ms delay to prevent multiple clicks
// Debounced delete
debouncedDeleteInterest = debounce(async () => {
await deleteInterest();
}, 300); // 300ms delay to prevent multiple clicks
};
// Initialize on component creation
initializeDebouncedFunctions();
// Watch for changes to trigger auto-save
watch(
@ -709,7 +749,37 @@ watch(
(newValue, oldValue) => {
if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
hasUnsavedChanges.value = true;
autoSave();
// Cancel any pending saves
if (debouncedSaveInterest) debouncedSaveInterest.cancel();
if (autoSave) autoSave();
}
},
{ deep: true }
);
// Watch yacht information and berth size to auto-set Sales Process Level
watch(
() => {
if (!interest.value) return null;
return {
yachtName: interest.value['Yacht Name'],
length: interest.value.Length,
width: interest.value.Width,
depth: interest.value.Depth,
berthSize: interest.value['Berth Size Desired']
};
},
(newValues) => {
if (!newValues || !interest.value) return;
// Check if any yacht information is provided or berth size is provided
const hasYachtInfo = !!(newValues.yachtName || newValues.length || newValues.width || newValues.depth);
const hasBerthSize = !!newValues.berthSize;
if ((hasYachtInfo || hasBerthSize) && interest.value['Sales Process Level'] === 'General Qualified Interest') {
// Auto-set to Specific Qualified Interest
interest.value['Sales Process Level'] = 'Specific Qualified Interest';
console.log('Auto-setting Sales Process Level to Specific Qualified Interest');
}
},
{ deep: true }
@ -761,7 +831,11 @@ const closeModal = () => {
};
const handleFormSubmit = () => {
saveInterest();
if (debouncedSaveInterest) {
debouncedSaveInterest();
} else {
saveInterest();
}
};
const saveInterest = async (isAutoSave = false) => {
@ -884,6 +958,34 @@ const eoiSendToSales = async () => {
}
};
// Group berths by first letter
const groupedBerths = computed(() => {
const grouped: Record<string, Berth[]> = {};
const sortedBerths = [...availableBerths.value].sort((a, b) =>
(a['Mooring Number'] || '').localeCompare(b['Mooring Number'] || '')
);
sortedBerths.forEach(berth => {
const firstLetter = (berth['Mooring Number'] || '').charAt(0).toUpperCase();
if (!grouped[firstLetter]) {
grouped[firstLetter] = [];
}
grouped[firstLetter].push(berth);
});
// Create flat array with dividers
const result: any[] = [];
Object.keys(grouped).sort().forEach((letter, index) => {
if (index > 0) {
// Add divider
result.push({ isDivider: true, letter });
}
result.push(...grouped[letter]);
});
return result;
});
// Load all available berths
const loadAvailableBerths = async () => {
loadingBerths.value = true;
@ -1120,7 +1222,11 @@ 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();
if (debouncedDeleteInterest) {
debouncedDeleteInterest();
} else {
deleteInterest();
}
}
};

View File

@ -1,34 +1,68 @@
import { deleteInterest } from '~/server/utils/nocodb';
import { deleteInterest, getInterestById } from '~/server/utils/nocodb';
export default defineEventHandler(async (event) => {
const startTime = Date.now();
const xTagHeader = getRequestHeader(event, "x-tag");
console.log('[delete-interest] Request received with x-tag:', xTagHeader);
console.log('[delete-interest] =========================');
console.log('[delete-interest] Request received at:', new Date().toISOString());
console.log('[delete-interest] x-tag:', xTagHeader);
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
console.error('[delete-interest] Authentication failed - invalid x-tag:', xTagHeader);
console.log('[delete-interest] Duration:', Date.now() - startTime, 'ms');
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
}
try {
const body = await readBody(event);
const { id } = body;
console.log('[delete-interest] Request body:', { id });
console.log('[delete-interest] Request body:', JSON.stringify(body, null, 2));
if (!id) {
console.error('[delete-interest] Missing ID in request');
console.log('[delete-interest] Duration:', Date.now() - startTime, 'ms');
throw createError({ statusCode: 400, statusMessage: "ID is required" });
}
console.log('[delete-interest] Deleting interest:', id);
// Pre-delete verification
console.log('[delete-interest] Pre-delete verification - checking if record exists...');
try {
const existingRecord = await getInterestById(id);
console.log('[delete-interest] Record found:', {
id: existingRecord.Id,
name: existingRecord['Full Name'],
email: existingRecord['Email Address']
});
} catch (verifyError: any) {
console.error('[delete-interest] Pre-delete verification failed:', verifyError);
if (verifyError.statusCode === 404 || verifyError.status === 404) {
console.error('[delete-interest] Record does not exist - cannot delete');
throw createError({ statusCode: 404, statusMessage: "Record not found" });
}
}
console.log('[delete-interest] Proceeding with deletion for ID:', id);
const result = await deleteInterest(id);
console.log('[delete-interest] Successfully deleted interest:', id);
console.log('[delete-interest] Delete operation completed successfully');
console.log('[delete-interest] Result:', JSON.stringify(result, null, 2));
console.log('[delete-interest] Duration:', Date.now() - startTime, 'ms');
console.log('[delete-interest] =========================');
return result;
} catch (error) {
console.error('[delete-interest] Error occurred:', error);
console.error('[delete-interest] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
} catch (error: any) {
console.error('[delete-interest] =========================');
console.error('[delete-interest] ERROR OCCURRED');
console.error('[delete-interest] Error type:', error.constructor.name);
console.error('[delete-interest] Error message:', error.message);
console.error('[delete-interest] Error status:', error.statusCode || error.status || 'unknown');
console.error('[delete-interest] Error stack:', error.stack || 'No stack trace');
console.error('[delete-interest] Full error object:', JSON.stringify(error, null, 2));
console.error('[delete-interest] Duration:', Date.now() - startTime, 'ms');
console.error('[delete-interest] =========================');
if (error instanceof Error) {
if (error.statusCode || error.status) {
throw error; // Re-throw if it's already a proper error
} else if (error instanceof Error) {
throw createError({ statusCode: 500, statusMessage: error.message });
} else {
throw createError({

View File

@ -84,7 +84,6 @@ export const updateInterest = async (id: string, data: Partial<Interest>, retryC
"Depth",
"Created At",
"Source",
"Place of Residence",
"Contact Method Preferred",
"Request Form Sent",
"Berth Number",
@ -198,7 +197,6 @@ export const createInterest = async (data: Partial<Interest>) => {
"Width",
"Depth",
"Source",
"Place of Residence",
"Contact Method Preferred",
"Lead Category",
"EOI Status",
@ -243,10 +241,27 @@ export const createInterest = async (data: Partial<Interest>) => {
};
export const deleteInterest = async (id: string) => {
console.log('[nocodb.deleteInterest] Deleting interest:', id);
const startTime = Date.now();
console.log('[nocodb.deleteInterest] =========================');
console.log('[nocodb.deleteInterest] DELETE operation started at:', new Date().toISOString());
console.log('[nocodb.deleteInterest] Target ID:', id);
const url = createTableUrl(Table.Interest);
console.log('[nocodb.deleteInterest] URL:', url);
const requestBody = {
"Id": parseInt(id)
};
console.log('[nocodb.deleteInterest] Request configuration:');
console.log(' Method: DELETE');
console.log(' URL:', url);
console.log(' Headers:', {
"xc-token": getNocoDbConfiguration().token ? "***" + getNocoDbConfiguration().token.slice(-4) : "not set",
"Content-Type": "application/json"
});
console.log(' Body:', JSON.stringify(requestBody, null, 2));
try {
// According to NocoDB API docs, DELETE requires ID in the body
const result = await $fetch(url, {
@ -255,15 +270,26 @@ export const deleteInterest = async (id: string) => {
"xc-token": getNocoDbConfiguration().token,
"Content-Type": "application/json"
},
body: {
"Id": parseInt(id)
}
body: requestBody
});
console.log('[nocodb.deleteInterest] Delete successful for ID:', id);
console.log('[nocodb.deleteInterest] DELETE successful');
console.log('[nocodb.deleteInterest] Response:', JSON.stringify(result, null, 2));
console.log('[nocodb.deleteInterest] Duration:', Date.now() - startTime, 'ms');
console.log('[nocodb.deleteInterest] =========================');
return result;
} catch (error) {
console.error('[nocodb.deleteInterest] Delete failed:', error);
console.error('[nocodb.deleteInterest] Error details:', error instanceof Error ? error.message : 'Unknown error');
} catch (error: any) {
console.error('[nocodb.deleteInterest] =========================');
console.error('[nocodb.deleteInterest] DELETE FAILED');
console.error('[nocodb.deleteInterest] Error type:', error.constructor.name);
console.error('[nocodb.deleteInterest] Error message:', error.message);
console.error('[nocodb.deleteInterest] Error status:', error.statusCode || error.status || 'unknown');
console.error('[nocodb.deleteInterest] Error data:', error.data);
console.error('[nocodb.deleteInterest] Error stack:', error.stack || 'No stack trace');
console.error('[nocodb.deleteInterest] Full error:', JSON.stringify(error, null, 2));
console.error('[nocodb.deleteInterest] Duration:', Date.now() - startTime, 'ms');
console.error('[nocodb.deleteInterest] =========================');
throw error;
}
};