Replace external berth dashboard with native Vue interface
- Replace iframe embed with full-featured berth status dashboard - Add BerthDetailsModal and BerthStatusBadge components - Implement search, filtering, and multiple view modes - Add berth management API endpoints (get-by-id, update) - Include measurement conversion utilities and type definitions - Provide status summaries and visual berth overview
This commit is contained in:
735
components/BerthDetailsModal.vue
Normal file
735
components/BerthDetailsModal.vue
Normal file
@@ -0,0 +1,735 @@
|
||||
<template>
|
||||
<v-dialog
|
||||
v-model="isVisible"
|
||||
:fullscreen="mobile"
|
||||
:max-width="mobile ? undefined : 1000"
|
||||
scrollable
|
||||
persistent
|
||||
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
||||
>
|
||||
<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-6">
|
||||
<v-container fluid class="pa-0">
|
||||
<!-- Basic Information Section -->
|
||||
<div class="section mb-6">
|
||||
<h4 class="text-h6 mb-3 text-primary">Basic Information</h4>
|
||||
<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="section mb-6">
|
||||
<h4 class="text-h6 mb-3 text-primary">Specifications</h4>
|
||||
<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.Depth"
|
||||
label="Depth (ft/m)"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
<div v-else class="field-display">
|
||||
<span class="field-label">Depth:</span>
|
||||
<span class="field-value">
|
||||
{{ displayMeasurement(berth.Depth) }}
|
||||
</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- Infrastructure Section -->
|
||||
<div class="section mb-6">
|
||||
<h4 class="text-h6 mb-3 text-primary">Infrastructure</h4>
|
||||
<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="section mb-6">
|
||||
<h4 class="text-h6 mb-3 text-primary">Hardware</h4>
|
||||
<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="section mb-6">
|
||||
<h4 class="text-h6 mb-3 text-primary">Details</h4>
|
||||
<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="section">
|
||||
<h4 class="text-h6 mb-3 text-primary">
|
||||
Interested Parties
|
||||
<v-chip
|
||||
v-if="interestedParties.length > 0"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ interestedParties.length }}
|
||||
</v-chip>
|
||||
</h4>
|
||||
|
||||
<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>
|
||||
.section {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.field-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.clickable-row {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.clickable-row:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
:deep(.v-table) th {
|
||||
font-weight: 600;
|
||||
color: #424242;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
</style>
|
||||
43
components/BerthStatusBadge.vue
Normal file
43
components/BerthStatusBadge.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<v-chip
|
||||
:color="statusColor"
|
||||
:size="size"
|
||||
variant="flat"
|
||||
class="font-weight-medium"
|
||||
>
|
||||
{{ status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { BerthStatus } from '@/utils/types';
|
||||
|
||||
interface Props {
|
||||
status: string;
|
||||
size?: 'x-small' | 'small' | 'default' | 'large' | 'x-large';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'small'
|
||||
});
|
||||
|
||||
const statusColor = computed(() => {
|
||||
switch (props.status) {
|
||||
case BerthStatus.Available:
|
||||
return 'success'; // Green
|
||||
case BerthStatus.Waitlist:
|
||||
return 'primary'; // Blue
|
||||
case BerthStatus.Reserved:
|
||||
return 'warning'; // Orange
|
||||
case BerthStatus.Sold:
|
||||
return 'error'; // Red
|
||||
default:
|
||||
return 'grey';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Additional styling if needed */
|
||||
</style>
|
||||
Reference in New Issue
Block a user