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:
2025-06-17 15:59:39 +02:00
parent 0b881a2588
commit 0e85cb40bc
9 changed files with 2167 additions and 36 deletions

View File

@@ -0,0 +1,45 @@
import { getNocoDbConfiguration } from "../utils/nocodb";
import { requireAuth } from "../utils/auth";
export default defineEventHandler(async (event) => {
console.log('[get-berth-by-id] Request received');
// Check authentication (x-tag header OR Keycloak session)
await requireAuth(event);
try {
const query = getQuery(event);
const berthId = query.id;
if (!berthId) {
throw createError({
statusCode: 400,
statusMessage: "Berth ID is required"
});
}
const config = getNocoDbConfiguration();
const berthsTableId = "mczgos9hr3oa9qc";
console.log('[get-berth-by-id] Fetching berth with ID:', berthId);
// Fetch berth with linked interested parties
const berth = await $fetch(`${config.url}/api/v2/tables/${berthsTableId}/records/${berthId}`, {
headers: {
"xc-token": config.token,
},
params: {
// Expand the "Interested Parties" linked field to get full interest records
fields: '*,Interested Parties.*'
}
});
console.log('[get-berth-by-id] Successfully fetched berth:', berthId);
return berth;
} catch (error) {
console.error('[get-berth-by-id] Error occurred:', error);
console.error('[get-berth-by-id] Error details:', error instanceof Error ? error.message : 'Unknown error');
throw error;
}
});

View File

@@ -18,16 +18,18 @@ export default defineEventHandler(async (event) => {
},
params: {
limit: 1000,
// Include interested parties count (expand the linked field)
fields: '*,Interested Parties.Id,Interested Parties.Full Name,Interested Parties.Sales Process Level,Interested Parties.EOI Status,Interested Parties.Contract Status'
},
});
console.log('[get-berths] Successfully fetched berths, count:', berths.list?.length || 0);
// Sort berths by letter zone and then by number
// Sort berths by letter zone and then by number using Mooring Number
if (berths.list && Array.isArray(berths.list)) {
berths.list.sort((a, b) => {
const berthA = a['Berth Number'] || '';
const berthB = b['Berth Number'] || '';
const berthA = a['Mooring Number'] || '';
const berthB = b['Mooring Number'] || '';
// Extract letter and number parts
const matchA = berthA.match(/^([A-Za-z]+)(\d+)$/);

114
server/api/update-berth.ts Normal file
View File

@@ -0,0 +1,114 @@
import { withBerthQueue } from '~/server/utils/operation-lock';
import { getNocoDbConfiguration } from '~/server/utils/nocodb';
import { requireAuth } from '~/server/utils/auth';
import {
BerthStatus,
BerthArea,
SidePontoon,
MooringType,
CleatType,
CleatCapacity,
BollardType,
BollardCapacity,
Access
} from '~/utils/types';
export default defineEventHandler(async (event) => {
console.log('[update-berth] Request received');
// Check authentication (x-tag header OR Keycloak session)
await requireAuth(event);
try {
const body = await readBody(event);
const { berthId, updates } = body;
console.log('[update-berth] Request body:', { berthId, updates });
if (!berthId || !updates) {
throw createError({
statusCode: 400,
statusMessage: "berthId and updates object are required"
});
}
// Validate enum fields
const validEnumFields: Record<string, string[]> = {
'Status': Object.values(BerthStatus),
'Area': Object.values(BerthArea),
'Side Pontoon': Object.values(SidePontoon),
'Mooring Type': Object.values(MooringType),
'Cleat Type': Object.values(CleatType),
'Cleat Capacity': Object.values(CleatCapacity),
'Bollard Type': Object.values(BollardType),
'Bollard Capacity': Object.values(BollardCapacity),
'Access': Object.values(Access)
};
// Validate enum values
for (const [field, value] of Object.entries(updates)) {
if (validEnumFields[field] && value !== null && value !== undefined) {
if (!validEnumFields[field].includes(value as string)) {
throw createError({
statusCode: 400,
statusMessage: `Invalid value for ${field}: ${value}. Must be one of: ${validEnumFields[field].join(', ')}`
});
}
}
}
// Handle measurement conversions
// If metric values are being updated, convert them to imperial for storage
const measurementFields = ['Nominal Boat Size', 'Water Depth', 'Length', 'Width', 'Depth'];
const processedUpdates = { ...updates };
for (const field of measurementFields) {
if (processedUpdates[field] !== undefined) {
const value = processedUpdates[field];
if (typeof value === 'string') {
// Parse user input and convert metric to imperial if needed
const cleanInput = value.replace(/[^\d.]/g, '');
const numericValue = parseFloat(cleanInput);
if (!isNaN(numericValue)) {
const isMetric = value.toLowerCase().includes('m') && !value.toLowerCase().includes('ft');
if (isMetric) {
// Convert metric to imperial for NocoDB storage
const imperial = numericValue / 0.3048;
processedUpdates[field] = parseFloat(imperial.toFixed(2));
console.log(`[update-berth] Converted ${field} from ${numericValue}m to ${processedUpdates[field]}ft`);
} else {
// Assume imperial, store as is
processedUpdates[field] = numericValue;
}
}
}
}
}
// Use queuing system to handle concurrent updates
return await withBerthQueue(berthId, async () => {
const config = getNocoDbConfiguration();
const berthsTableId = "mczgos9hr3oa9qc";
const url = `${config.url}/api/v2/tables/${berthsTableId}/records/${berthId}`;
console.log('[update-berth] URL:', url);
console.log('[update-berth] Processed updates:', processedUpdates);
const result = await $fetch(url, {
method: 'PATCH',
headers: {
"xc-token": config.token,
},
body: processedUpdates,
});
console.log('[update-berth] Successfully updated berth:', berthId);
return result;
});
} catch (error) {
console.error('[update-berth] Error occurred:', error);
console.error('[update-berth] Error details:', error instanceof Error ? error.message : 'Unknown error');
throw error;
}
});