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:
45
server/api/get-berth-by-id.ts
Normal file
45
server/api/get-berth-by-id.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
@@ -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
114
server/api/update-berth.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user