updates
This commit is contained in:
parent
5e4b20f6ae
commit
5c30411c2b
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue