836 lines
26 KiB
Vue
836 lines
26 KiB
Vue
<template>
|
|
<v-dialog
|
|
v-model="isVisible"
|
|
:fullscreen="mobile"
|
|
:max-width="mobile ? undefined : 1200"
|
|
scrollable
|
|
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
|
@click:outside="closeModal"
|
|
>
|
|
<v-card v-if="berth">
|
|
<v-card-title class="d-flex align-center justify-space-between">
|
|
<div>
|
|
<h3 class="text-h5">{{ berth['Mooring Number'] || 'Unknown Berth' }}</h3>
|
|
<p class="text-body-2 text-grey-darken-1 mb-0">
|
|
Area {{ berth.Area }} - {{ berth.Status }}
|
|
</p>
|
|
</div>
|
|
<div class="d-flex align-center gap-2">
|
|
<v-btn
|
|
v-if="!editMode"
|
|
@click="editMode = true"
|
|
color="primary"
|
|
variant="outlined"
|
|
size="small"
|
|
prepend-icon="mdi-pencil"
|
|
>
|
|
Edit
|
|
</v-btn>
|
|
<v-btn
|
|
v-if="editMode"
|
|
@click="saveChanges"
|
|
color="primary"
|
|
variant="flat"
|
|
size="small"
|
|
prepend-icon="mdi-content-save"
|
|
:loading="saving"
|
|
>
|
|
Save
|
|
</v-btn>
|
|
<v-btn
|
|
v-if="editMode"
|
|
@click="cancelEdit"
|
|
color="grey"
|
|
variant="outlined"
|
|
size="small"
|
|
prepend-icon="mdi-close"
|
|
>
|
|
Cancel
|
|
</v-btn>
|
|
<v-btn
|
|
@click="closeModal"
|
|
icon="mdi-close"
|
|
variant="text"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
</v-card-title>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-text class="pa-8 bg-grey-lighten-5">
|
|
<v-container fluid class="pa-0">
|
|
<!-- Basic Information Section -->
|
|
<div class="info-section mb-8">
|
|
<div class="section-header mb-6">
|
|
<v-icon color="primary" size="28" class="mr-3">mdi-information-outline</v-icon>
|
|
<h4 class="text-h5 text-primary font-weight-bold">Basic Information</h4>
|
|
</div>
|
|
<v-row>
|
|
<v-col cols="12" md="4">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model="editedBerth['Mooring Number']"
|
|
label="Mooring Number"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Mooring Number:</span>
|
|
<span class="field-value">{{ berth['Mooring Number'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="4">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth.Area"
|
|
:items="areaOptions"
|
|
label="Area"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Area:</span>
|
|
<span class="field-value">{{ berth.Area || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="4">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth.Status"
|
|
:items="statusOptions"
|
|
label="Status"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Status:</span>
|
|
<BerthStatusBadge :status="berth.Status" />
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<!-- Specifications Section -->
|
|
<div class="info-section mb-8">
|
|
<div class="section-header mb-6">
|
|
<v-icon color="primary" size="28" class="mr-3">mdi-ruler-square</v-icon>
|
|
<h4 class="text-h5 text-primary font-weight-bold">Specifications</h4>
|
|
</div>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model="editedBerth['Nominal Boat Size']"
|
|
label="Nominal Boat Size (ft/m)"
|
|
variant="outlined"
|
|
density="compact"
|
|
hint="Enter in feet or meters (e.g., '50ft' or '15.24m')"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Nominal Boat Size:</span>
|
|
<span class="field-value">
|
|
{{ displayMeasurement(berth['Nominal Boat Size']) }}
|
|
</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model="editedBerth['Water Depth']"
|
|
label="Water Depth (ft/m)"
|
|
variant="outlined"
|
|
density="compact"
|
|
hint="Enter in feet or meters"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Water Depth:</span>
|
|
<span class="field-value">
|
|
{{ displayMeasurement(berth['Water Depth']) }}
|
|
</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="4">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model="editedBerth.Length"
|
|
label="Length (ft/m)"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Length:</span>
|
|
<span class="field-value">
|
|
{{ displayMeasurement(berth.Length) }}
|
|
</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="4">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model="editedBerth.Width"
|
|
label="Width (ft/m)"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Width:</span>
|
|
<span class="field-value">
|
|
{{ displayMeasurement(berth.Width) }}
|
|
</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="4">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model="editedBerth.Draft"
|
|
label="Draft (ft/m)"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Draft:</span>
|
|
<span class="field-value">
|
|
{{ displayMeasurement(berth.Draft) }}
|
|
</span>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<!-- Infrastructure Section -->
|
|
<div class="info-section mb-8">
|
|
<div class="section-header mb-6">
|
|
<v-icon color="primary" size="28" class="mr-3">mdi-office-building-outline</v-icon>
|
|
<h4 class="text-h5 text-primary font-weight-bold">Infrastructure</h4>
|
|
</div>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth['Side Pontoon']"
|
|
:items="sidePontoonOptions"
|
|
label="Side Pontoon"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Side Pontoon:</span>
|
|
<span class="field-value">{{ berth['Side Pontoon'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="3">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model.number="editedBerth['Power Capacity']"
|
|
label="Power Capacity (A)"
|
|
variant="outlined"
|
|
density="compact"
|
|
type="number"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Power Capacity:</span>
|
|
<span class="field-value">{{ berth['Power Capacity'] }}A</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="3">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model.number="editedBerth.Voltage"
|
|
label="Voltage (V)"
|
|
variant="outlined"
|
|
density="compact"
|
|
type="number"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Voltage:</span>
|
|
<span class="field-value">{{ berth.Voltage }}V</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth['Mooring Type']"
|
|
:items="mooringTypeOptions"
|
|
label="Mooring Type"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Mooring Type:</span>
|
|
<span class="field-value">{{ berth['Mooring Type'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth.Access"
|
|
:items="accessOptions"
|
|
label="Access"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Access:</span>
|
|
<span class="field-value">{{ berth.Access || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<!-- Hardware Section -->
|
|
<div class="info-section mb-8">
|
|
<div class="section-header mb-6">
|
|
<v-icon color="primary" size="28" class="mr-3">mdi-wrench-outline</v-icon>
|
|
<h4 class="text-h5 text-primary font-weight-bold">Hardware</h4>
|
|
</div>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth['Cleat Type']"
|
|
:items="cleatTypeOptions"
|
|
label="Cleat Type"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Cleat Type:</span>
|
|
<span class="field-value">{{ berth['Cleat Type'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth['Cleat Capacity']"
|
|
:items="cleatCapacityOptions"
|
|
label="Cleat Capacity"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Cleat Capacity:</span>
|
|
<span class="field-value">{{ berth['Cleat Capacity'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth['Bollard Type']"
|
|
:items="bollardTypeOptions"
|
|
label="Bollard Type"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Bollard Type:</span>
|
|
<span class="field-value">{{ berth['Bollard Type'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-select
|
|
v-if="editMode"
|
|
v-model="editedBerth['Bollard Capacity']"
|
|
:items="bollardCapacityOptions"
|
|
label="Bollard Capacity"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Bollard Capacity:</span>
|
|
<span class="field-value">{{ berth['Bollard Capacity'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<!-- Pricing & Orientation -->
|
|
<div class="info-section mb-8">
|
|
<div class="section-header mb-6">
|
|
<v-icon color="primary" size="28" class="mr-3">mdi-currency-usd</v-icon>
|
|
<h4 class="text-h5 text-primary font-weight-bold">Pricing & Details</h4>
|
|
</div>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model.number="editedBerth.Price"
|
|
label="Price"
|
|
variant="outlined"
|
|
density="compact"
|
|
type="number"
|
|
prefix="$"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Price:</span>
|
|
<span class="field-value">${{ formatPrice(berth.Price) }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="12" md="6">
|
|
<v-text-field
|
|
v-if="editMode"
|
|
v-model="editedBerth['Bow Facing']"
|
|
label="Bow Facing"
|
|
variant="outlined"
|
|
density="compact"
|
|
/>
|
|
<div v-else class="field-display">
|
|
<span class="field-label">Bow Facing:</span>
|
|
<span class="field-value">{{ berth['Bow Facing'] || '—' }}</span>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<!-- Interested Parties Section -->
|
|
<div class="info-section">
|
|
<div class="section-header mb-6">
|
|
<v-icon color="primary" size="28" class="mr-3">mdi-account-group-outline</v-icon>
|
|
<h4 class="text-h5 text-primary font-weight-bold">
|
|
Interested Parties
|
|
<v-chip
|
|
v-if="interestedParties.length > 0"
|
|
size="small"
|
|
color="primary"
|
|
variant="tonal"
|
|
class="ml-3"
|
|
>
|
|
{{ interestedParties.length }}
|
|
</v-chip>
|
|
</h4>
|
|
</div>
|
|
|
|
<div v-if="interestedParties.length === 0" class="text-center py-4 text-grey-darken-1">
|
|
<v-icon size="48" class="mb-2">mdi-account-search-outline</v-icon>
|
|
<p>No interested parties</p>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<v-card variant="outlined" class="overflow-hidden">
|
|
<v-table>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Sales Status</th>
|
|
<th>EOI Status</th>
|
|
<th>Contract Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="party in interestedParties"
|
|
:key="party.Id"
|
|
class="clickable-row"
|
|
@click="viewInterest(party)"
|
|
>
|
|
<td>
|
|
<div class="d-flex align-center">
|
|
<v-avatar size="24" color="primary" class="mr-2">
|
|
<span class="text-caption">{{ getInitials(party['Full Name']) }}</span>
|
|
</v-avatar>
|
|
<span class="font-weight-medium">{{ party['Full Name'] }}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<InterestSalesBadge
|
|
v-if="party['Sales Process Level']"
|
|
:salesProcessLevel="party['Sales Process Level']"
|
|
size="small"
|
|
/>
|
|
<span v-else class="text-grey-darken-1">—</span>
|
|
</td>
|
|
<td>
|
|
<EOIStatusBadge
|
|
v-if="party['EOI Status']"
|
|
:eoiStatus="party['EOI Status']"
|
|
size="small"
|
|
/>
|
|
<span v-else class="text-grey-darken-1">—</span>
|
|
</td>
|
|
<td>
|
|
<ContractStatusBadge
|
|
v-if="party['Contract Status']"
|
|
:contractStatus="party['Contract Status']"
|
|
size="small"
|
|
/>
|
|
<span v-else class="text-grey-darken-1">—</span>
|
|
</td>
|
|
<td>
|
|
<v-btn
|
|
@click.stop="viewInterest(party)"
|
|
icon="mdi-open-in-new"
|
|
size="small"
|
|
variant="text"
|
|
color="primary"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
</v-card>
|
|
</div>
|
|
</div>
|
|
</v-container>
|
|
</v-card-text>
|
|
</v-card>
|
|
|
|
<!-- Interest Details Modal -->
|
|
<InterestDetailsModal
|
|
v-model="showInterestModal"
|
|
:selected-interest="selectedInterest"
|
|
@save="handleInterestSave"
|
|
/>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
|
import type { Berth, Interest, InterestedParty } from '@/utils/types';
|
|
import {
|
|
BerthStatus,
|
|
BerthArea,
|
|
SidePontoon,
|
|
MooringType,
|
|
CleatType,
|
|
CleatCapacity,
|
|
BollardType,
|
|
BollardCapacity,
|
|
Access
|
|
} from '@/utils/types';
|
|
import BerthStatusBadge from './BerthStatusBadge.vue';
|
|
import InterestSalesBadge from './InterestSalesBadge.vue';
|
|
import EOIStatusBadge from './EOIStatusBadge.vue';
|
|
import ContractStatusBadge from './ContractStatusBadge.vue';
|
|
import InterestDetailsModal from './InterestDetailsModal.vue';
|
|
|
|
// Simple debounce implementation
|
|
function debounce<T extends (...args: any[]) => any>(
|
|
func: T,
|
|
wait: number
|
|
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
|
|
let timeout: NodeJS.Timeout | null = null;
|
|
|
|
const debounced = (...args: Parameters<T>) => {
|
|
if (timeout) clearTimeout(timeout);
|
|
timeout = setTimeout(() => func(...args), wait);
|
|
};
|
|
|
|
debounced.cancel = () => {
|
|
if (timeout) clearTimeout(timeout);
|
|
};
|
|
|
|
return debounced;
|
|
}
|
|
|
|
interface Props {
|
|
modelValue: boolean;
|
|
berth: Berth | null;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:modelValue', value: boolean): void;
|
|
(e: 'save', berth: Berth): void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<Emits>();
|
|
|
|
const { mobile } = useDisplay();
|
|
const toast = useToast();
|
|
|
|
const isVisible = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => emit('update:modelValue', value)
|
|
});
|
|
|
|
const editMode = ref(false);
|
|
const saving = ref(false);
|
|
const editedBerth = ref<any>({});
|
|
const showInterestModal = ref(false);
|
|
const selectedInterest = ref<Interest | null>(null);
|
|
|
|
// Auto-save related
|
|
const hasUnsavedChanges = ref(false);
|
|
const autoSaveTimer = ref<NodeJS.Timeout | null>(null);
|
|
|
|
// Store the debounced functions
|
|
let autoSave: any = null;
|
|
let debouncedSaveChanges: any = null;
|
|
|
|
// Initialize debounced functions
|
|
const initializeDebouncedFunctions = () => {
|
|
// Auto-save function (debounced)
|
|
autoSave = debounce(async () => {
|
|
if (!hasUnsavedChanges.value || !editedBerth.value || saving.value || !editMode.value) return;
|
|
|
|
console.log('Auto-saving berth...');
|
|
await saveChanges(true); // Pass true to indicate auto-save
|
|
}, 2000); // 2 second delay
|
|
|
|
// Debounced manual save
|
|
debouncedSaveChanges = debounce(async () => {
|
|
await saveChanges();
|
|
}, 300); // 300ms delay to prevent multiple clicks
|
|
};
|
|
|
|
// Initialize on component creation
|
|
initializeDebouncedFunctions();
|
|
|
|
// Dropdown options
|
|
const areaOptions = Object.values(BerthArea);
|
|
const statusOptions = Object.values(BerthStatus);
|
|
const sidePontoonOptions = Object.values(SidePontoon);
|
|
const mooringTypeOptions = Object.values(MooringType);
|
|
const cleatTypeOptions = Object.values(CleatType);
|
|
const cleatCapacityOptions = Object.values(CleatCapacity);
|
|
const bollardTypeOptions = Object.values(BollardType);
|
|
const bollardCapacityOptions = Object.values(BollardCapacity);
|
|
const accessOptions = Object.values(Access);
|
|
|
|
const interestedParties = computed(() => {
|
|
return props.berth?.['Interested Parties'] || [];
|
|
});
|
|
|
|
// Display measurement with both metric and imperial
|
|
const displayMeasurement = (imperial: number): string => {
|
|
if (!imperial) return '—';
|
|
const metric = imperial * 0.3048;
|
|
return `${metric.toFixed(2)}m / ${imperial}ft`;
|
|
};
|
|
|
|
// Format price with thousands separator
|
|
const formatPrice = (price: number): string => {
|
|
if (!price) return '0';
|
|
return new Intl.NumberFormat('en-US').format(price);
|
|
};
|
|
|
|
// Get initials for avatar
|
|
const getInitials = (name: string): 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();
|
|
};
|
|
|
|
// Watch for changes to trigger auto-save
|
|
watch(
|
|
() => editedBerth.value,
|
|
(newValue, oldValue) => {
|
|
if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue) && editMode.value) {
|
|
hasUnsavedChanges.value = true;
|
|
// Cancel any pending saves
|
|
if (debouncedSaveChanges) debouncedSaveChanges.cancel();
|
|
if (autoSave) autoSave();
|
|
}
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// Reset edit mode when berth changes
|
|
watch(() => props.berth, (newBerth) => {
|
|
if (newBerth) {
|
|
editedBerth.value = { ...newBerth };
|
|
hasUnsavedChanges.value = false;
|
|
}
|
|
editMode.value = false;
|
|
}, { immediate: true });
|
|
|
|
const cancelEdit = () => {
|
|
editMode.value = false;
|
|
if (props.berth) {
|
|
editedBerth.value = { ...props.berth };
|
|
}
|
|
};
|
|
|
|
const saveChanges = async (isAutoSave = false) => {
|
|
if (!props.berth || !editedBerth.value) return;
|
|
|
|
saving.value = true;
|
|
try {
|
|
await $fetch('/api/update-berth', {
|
|
method: 'POST',
|
|
body: {
|
|
berthId: props.berth.Id,
|
|
updates: editedBerth.value
|
|
}
|
|
});
|
|
|
|
hasUnsavedChanges.value = false;
|
|
|
|
if (!isAutoSave) {
|
|
toast.success("Berth saved successfully!");
|
|
emit('save', { ...props.berth, ...editedBerth.value });
|
|
editMode.value = false;
|
|
} else {
|
|
// For auto-save, just emit save to refresh parent
|
|
emit('save', { ...props.berth, ...editedBerth.value });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save berth:', error);
|
|
if (!isAutoSave) {
|
|
toast.error("Failed to save berth. Please try again.");
|
|
}
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
};
|
|
|
|
const closeModal = () => {
|
|
isVisible.value = false;
|
|
editMode.value = false;
|
|
};
|
|
|
|
const viewInterest = (interest: InterestedParty) => {
|
|
selectedInterest.value = interest;
|
|
showInterestModal.value = true;
|
|
};
|
|
|
|
const handleInterestSave = (interest: Interest) => {
|
|
// Refresh berth data to get updated interest information
|
|
// This could trigger a refresh in the parent component
|
|
showInterestModal.value = false;
|
|
};
|
|
|
|
// Cleanup on unmount
|
|
onUnmounted(() => {
|
|
if (autoSaveTimer.value) {
|
|
clearTimeout(autoSaveTimer.value);
|
|
}
|
|
// Cancel any pending auto-save
|
|
if (autoSave) autoSave.cancel();
|
|
if (debouncedSaveChanges) debouncedSaveChanges.cancel();
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Modern card style with subtle shadow */
|
|
:deep(.v-dialog .v-card) {
|
|
box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.18) !important;
|
|
}
|
|
|
|
/* Header styling */
|
|
:deep(.v-card-title) {
|
|
background-color: #fafafa;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
padding: 24px !important;
|
|
}
|
|
|
|
/* Modern section styling with subtle shadows */
|
|
.info-section {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 32px;
|
|
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.08), 0 1px 3px 0 rgba(0, 0, 0, 0.04);
|
|
transition: all 0.3s ease;
|
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.info-section:hover {
|
|
box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.12), 0 4px 8px -2px rgba(0, 0, 0, 0.08);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* Section headers with icons */
|
|
.section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
border-bottom: 2px solid #f1f5f9;
|
|
padding-bottom: 16px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.section-header h4 {
|
|
margin-bottom: 0 !important;
|
|
color: #1e293b;
|
|
font-size: 1.375rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.section-header .v-icon {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white !important;
|
|
border-radius: 50%;
|
|
padding: 8px;
|
|
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.25);
|
|
}
|
|
|
|
/* Field display improvements */
|
|
.field-display {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding: 12px 0;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.field-display:hover {
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.field-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.field-value {
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: #1e293b;
|
|
}
|
|
|
|
/* Interested parties table styling */
|
|
:deep(.v-table) {
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
:deep(.v-table) th {
|
|
font-weight: 600;
|
|
color: #475569;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
background-color: #f8fafc !important;
|
|
border-bottom: 2px solid #e2e8f0 !important;
|
|
}
|
|
|
|
.clickable-row {
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.clickable-row:hover {
|
|
background-color: #f8fafc;
|
|
transform: scale(1.01);
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
/* Button styling */
|
|
:deep(.v-btn) {
|
|
font-weight: 500;
|
|
letter-spacing: 0.025em;
|
|
}
|
|
|
|
/* Smooth scrolling */
|
|
:deep(.v-card-text) {
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
/* Responsive padding adjustments */
|
|
@media (max-width: 960px) {
|
|
.section {
|
|
padding: 16px;
|
|
}
|
|
|
|
:deep(.v-card-title) {
|
|
padding: 16px !important;
|
|
}
|
|
}
|
|
</style>
|