FEAT: Enhance berth color handling in dashboard components and improve authentication middleware with caching
This commit is contained in:
parent
8a4824e6fe
commit
b585daddde
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue