FEAT: Enhance berth color handling in dashboard components and improve authentication middleware with caching

This commit is contained in:
Matt 2025-06-17 18:05:22 +02:00
parent 8a4824e6fe
commit b585daddde
5 changed files with 202 additions and 32 deletions

View File

@ -2,10 +2,10 @@
<v-dialog
v-model="isVisible"
:fullscreen="mobile"
:max-width="mobile ? undefined : 1000"
:max-width="mobile ? undefined : 1200"
scrollable
persistent
: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">
@ -688,48 +688,122 @@ onUnmounted(() => {
</script>
<style scoped>
.section {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
background-color: #fafafa;
/* 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 */
.section {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: all 0.3s ease;
}
.section:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* Section titles with accent underline */
.section h4 {
position: relative;
padding-bottom: 12px;
margin-bottom: 24px !important;
}
.section h4::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 3px;
background-color: var(--v-primary-base);
border-radius: 2px;
}
/* Field display improvements */
.field-display {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 0;
gap: 6px;
padding: 12px 0;
transition: all 0.2s ease;
}
.field-display:hover {
transform: translateX(4px);
}
.field-label {
font-size: 0.875rem;
font-weight: 500;
color: #666;
font-size: 0.75rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
letter-spacing: 0.05em;
}
.field-value {
font-size: 1rem;
font-weight: 400;
color: #333;
font-weight: 500;
color: #1e293b;
}
.clickable-row {
cursor: pointer;
transition: background-color 0.2s ease;
}
.clickable-row:hover {
background-color: #f5f5f5;
/* Interested parties table styling */
:deep(.v-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.v-table) th {
font-weight: 600;
color: #424242;
font-size: 0.875rem;
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>

View File

@ -10,11 +10,48 @@ export default defineNuxtRouteMiddleware(async (to) => {
return;
}
// Skip auth check if we're already on the login page to prevent redirect loops
if (to.path === '/login' || to.path.startsWith('/auth')) {
return;
}
console.log('[MIDDLEWARE] Checking authentication for route:', to.path);
// Use a cached auth state to avoid excessive API calls
const nuxtApp = useNuxtApp();
const cacheKey = 'auth:session:cache';
const cacheExpiry = 30000; // 30 seconds cache
// Check if we have a cached session
const cachedSession = nuxtApp.payload.data[cacheKey];
const now = Date.now();
if (cachedSession && cachedSession.timestamp && (now - cachedSession.timestamp) < cacheExpiry) {
console.log('[MIDDLEWARE] Using cached session');
if (cachedSession.authenticated && cachedSession.user) {
return;
}
return navigateTo('/login');
}
try {
// Check Keycloak authentication via session API
const sessionData = await $fetch('/api/auth/session') as any;
// Check Keycloak authentication via session API with timeout
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); // 5 second timeout
const sessionData = await $fetch('/api/auth/session', {
signal: controller.signal,
retry: 1,
retryDelay: 500
}) as any;
clearTimeout(timeout);
// Cache the session data
nuxtApp.payload.data[cacheKey] = {
...sessionData,
timestamp: now
};
console.log('[MIDDLEWARE] Session check result:', {
authenticated: sessionData.authenticated,
@ -30,8 +67,21 @@ export default defineNuxtRouteMiddleware(async (to) => {
console.log('[MIDDLEWARE] No valid authentication found, redirecting to login');
return navigateTo('/login');
} catch (error) {
} catch (error: any) {
console.error('[MIDDLEWARE] Auth check failed:', error);
// If it's a network error or timeout, check if we have a recent cached session
if (error.name === 'AbortError' || error.code === 'ECONNREFUSED') {
console.log('[MIDDLEWARE] Network error, checking for recent cache');
const recentCache = nuxtApp.payload.data[cacheKey];
if (recentCache && recentCache.timestamp && (now - recentCache.timestamp) < 300000) { // 5 minutes
console.log('[MIDDLEWARE] Using recent cache despite network error');
if (recentCache.authenticated && recentCache.user) {
return;
}
}
}
return navigateTo('/login');
}
});

View File

@ -188,7 +188,11 @@
<!-- Berth Header -->
<div class="d-flex align-center justify-space-between mb-3">
<div class="d-flex align-center">
<v-avatar size="40" color="primary" class="mr-3">
<v-avatar
size="40"
:color="getBerthColorFromMooringNumber(berth['Mooring Number'])"
class="mr-3"
>
<span class="text-white text-body-2 font-weight-bold">
{{ berth['Mooring Number'] }}
</span>
@ -272,7 +276,11 @@
>
<td>
<div class="d-flex align-center">
<v-avatar size="32" color="primary" class="mr-3">
<v-avatar
size="32"
:color="getBerthColorFromMooringNumber(berth['Mooring Number'])"
class="mr-3"
>
<span class="text-white text-caption font-weight-bold">
{{ berth['Mooring Number'] }}
</span>
@ -326,6 +334,7 @@ import { ref, computed, onMounted } from 'vue';
import { useFetch } from '#app';
import type { Berth, BerthsResponse } from '@/utils/types';
import { BerthArea } from '@/utils/types';
import { getBerthColorFromMooringNumber } from '@/utils/berthColors';
import BerthStatusBadge from '@/components/BerthStatusBadge.vue';
import BerthDetailsModal from '@/components/BerthDetailsModal.vue';

View File

@ -115,8 +115,8 @@
</v-chip>
</v-card-title>
<v-divider />
<v-card-text class="pa-2" style="max-height: 600px; overflow-y: auto;">
<div class="d-flex flex-column gap-2">
<v-card-text class="pa-3" style="max-height: 600px; overflow-y: auto;">
<div class="d-flex flex-column gap-3">
<v-card
v-for="berth in getBerthsByStatus(status.value)"
:key="berth.Id"
@ -197,7 +197,11 @@
<!-- Berth Header -->
<div class="d-flex align-center justify-space-between mb-3">
<div class="d-flex align-center">
<v-avatar size="36" color="primary" class="mr-3">
<v-avatar
size="36"
:color="getBerthColorFromMooringNumber(berth['Mooring Number'])"
class="mr-3"
>
<span class="text-white text-body-2 font-weight-bold">
{{ berth['Mooring Number'] }}
</span>
@ -261,6 +265,7 @@ import { ref, computed } from 'vue';
import { useFetch } from '#app';
import type { Berth, BerthsResponse } from '@/utils/types';
import { BerthArea, BerthStatus } from '@/utils/types';
import { getBerthColorFromMooringNumber } from '@/utils/berthColors';
import BerthStatusBadge from '@/components/BerthStatusBadge.vue';
import BerthDetailsModal from '@/components/BerthDetailsModal.vue';

32
utils/berthColors.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* Berth area color mapping based on the Port Nimara layout
*/
export const berthAreaColors: Record<string, string> = {
'A': '#E67E22', // Orange/Brown
'B': '#F1C40F', // Yellow
'C': '#27AE60', // Green
'D': '#3498DB', // Blue
'E': '#E91E63', // Pink
'F': '#9B59B6', // Purple (if exists)
'G': '#1ABC9C', // Teal (if exists)
};
/**
* Get the color for a berth area
* @param area - The area letter
* @returns The hex color code for the area
*/
export function getBerthAreaColor(area: string): string {
return berthAreaColors[area?.toUpperCase()] || '#757575'; // Grey fallback
}
/**
* Get the color from a mooring number (e.g., "A1" -> orange)
* @param mooringNumber - The full mooring number
* @returns The hex color code for the area
*/
export function getBerthColorFromMooringNumber(mooringNumber: string): string {
if (!mooringNumber) return '#757575'; // Grey fallback
const area = mooringNumber.charAt(0).toUpperCase();
return getBerthAreaColor(area);
}