feat: Improve UI styling in ExpenseDetailsModal and ExpenseList, enhance authentication middleware caching, and optimize PDF generation for receipt fetching
This commit is contained in:
parent
3ba8542e4f
commit
6ebe96bbf4
|
|
@ -28,7 +28,7 @@
|
|||
{{ expense.DisplayPrice || expense.Price }}
|
||||
</div>
|
||||
<div v-if="expense.ConversionRate && expense.ConversionRate !== 1" class="conversion-info">
|
||||
<span class="text-caption text-grey-darken-1">
|
||||
<span class="text-caption text-grey-darken-3">
|
||||
Rate: {{ expense.ConversionRate }} | USD: {{ expense.DisplayPriceUSD }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -64,12 +64,12 @@
|
|||
|
||||
<!-- Multiple receipts indicator -->
|
||||
<v-chip
|
||||
v-if="expense.Receipt.length > 1"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
class="receipt-count-chip"
|
||||
variant="flat"
|
||||
:color="getCategoryColor(expense.Category)"
|
||||
class="text-caption text-grey-darken-3"
|
||||
>
|
||||
+{{ expense.Receipt.length - 1 }}
|
||||
{{ expense.Category || 'Other' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|||
// Use a cached auth state to avoid excessive API calls
|
||||
const nuxtApp = useNuxtApp();
|
||||
const cacheKey = 'auth:session:cache';
|
||||
const cacheExpiry = 5 * 60 * 1000; // 5 minutes cache (increased from 30 seconds)
|
||||
const cacheExpiry = 15 * 60 * 1000; // 15 minutes cache (increased for better UX)
|
||||
|
||||
// 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');
|
||||
console.log('[MIDDLEWARE] Using cached session (age:', Math.round((now - cachedSession.timestamp) / 1000), 'seconds)');
|
||||
if (cachedSession.authenticated && cachedSession.user) {
|
||||
// Store auth state for components
|
||||
if (!nuxtApp.payload.data) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<v-card class="mb-6">
|
||||
<v-card-text class="pa-6">
|
||||
<v-row align="center" class="mb-0">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-text-field
|
||||
v-model="filters.startDate"
|
||||
type="date"
|
||||
|
|
@ -32,11 +32,10 @@
|
|||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
@change="fetchExpenses"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-text-field
|
||||
v-model="filters.endDate"
|
||||
type="date"
|
||||
|
|
@ -44,11 +43,10 @@
|
|||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
@change="fetchExpenses"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-select
|
||||
v-model="filters.category"
|
||||
:items="['', 'Food/Drinks', 'Shop', 'Online', 'Other']"
|
||||
|
|
@ -57,11 +55,23 @@
|
|||
density="comfortable"
|
||||
hide-details
|
||||
clearable
|
||||
@update:model-value="fetchExpenses"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-btn
|
||||
@click="fetchExpenses"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
size="large"
|
||||
class="w-100"
|
||||
prepend-icon="mdi-magnify"
|
||||
>
|
||||
Apply
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6" md="2">
|
||||
<v-btn
|
||||
@click="resetToCurrentMonth"
|
||||
variant="outlined"
|
||||
|
|
@ -187,7 +197,7 @@
|
|||
<div class="d-flex flex-wrap align-center">
|
||||
<span class="text-subtitle-1 font-weight-medium mr-6">Export Options:</span>
|
||||
|
||||
<div class="d-flex gap-4">
|
||||
<div class="d-flex gap-6">
|
||||
<v-btn
|
||||
@click="exportCSV"
|
||||
:disabled="selectedExpenses.length === 0"
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-4" style="max-height: 600px; overflow-y: auto;">
|
||||
<div class="d-flex flex-column gap-6">
|
||||
<div class="d-flex flex-column gap-4">
|
||||
<v-card
|
||||
v-for="berth in getBerthsByStatus(status.value)"
|
||||
:key="berth.Id"
|
||||
|
|
@ -137,14 +137,24 @@
|
|||
</div>
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="text-body-2 font-weight-medium">${{ formatPrice(berth.Price) }}</span>
|
||||
<v-chip
|
||||
v-if="getInterestedCount(berth)"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getInterestedCount(berth) }} interested
|
||||
</v-chip>
|
||||
<v-tooltip v-if="getInterestedCount(berth)" location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-chip
|
||||
v-bind="props"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getInterestedCount(berth) }} interested
|
||||
</v-chip>
|
||||
</template>
|
||||
<div class="pa-2">
|
||||
<div class="text-subtitle-2 mb-1">Interested Parties:</div>
|
||||
<div v-for="party in berth['Interested Parties']" :key="party.Id" class="text-body-2">
|
||||
{{ party['Full Name'] }}
|
||||
</div>
|
||||
</div>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export default defineNuxtPlugin(() => {
|
|||
refreshTimer = null
|
||||
}
|
||||
|
||||
// Calculate time until refresh (refresh 5 minutes before expiry)
|
||||
const refreshBuffer = 5 * 60 * 1000 // 5 minutes in milliseconds
|
||||
// Calculate time until refresh (refresh 10 minutes before expiry for better safety margin)
|
||||
const refreshBuffer = 10 * 60 * 1000 // 10 minutes in milliseconds (increased from 5)
|
||||
const timeUntilRefresh = expiresAt - Date.now() - refreshBuffer
|
||||
|
||||
console.log('[AUTH_REFRESH] Scheduling token refresh in:', Math.max(0, timeUntilRefresh), 'ms')
|
||||
|
|
|
|||
|
|
@ -748,56 +748,22 @@ async function fetchReceiptImage(receipt: any): Promise<Buffer | null> {
|
|||
console.log('[expenses/generate-pdf] Detected S3 URL, fetching directly...');
|
||||
|
||||
try {
|
||||
// Ensure URL is properly encoded
|
||||
let encodedUrl = rawPath;
|
||||
try {
|
||||
// Parse and reconstruct URL to ensure proper encoding
|
||||
const url = new URL(rawPath);
|
||||
// Re-encode the pathname to handle special characters
|
||||
url.pathname = url.pathname.split('/').map(segment => encodeURIComponent(decodeURIComponent(segment))).join('/');
|
||||
encodedUrl = url.toString();
|
||||
console.log('[expenses/generate-pdf] URL encoded:', encodedUrl);
|
||||
} catch (urlError) {
|
||||
console.log('[expenses/generate-pdf] Using original URL (encoding failed):', rawPath);
|
||||
encodedUrl = rawPath;
|
||||
}
|
||||
// Use the signed URL directly without modification to preserve AWS signature
|
||||
console.log('[expenses/generate-pdf] Fetching from S3 URL (preserving signature):', rawPath);
|
||||
|
||||
// Fetch image directly from S3 URL with proper headers
|
||||
const response = await fetch(encodedUrl, {
|
||||
// Fetch image directly from S3 URL with minimal headers to avoid signature issues
|
||||
const response = await fetch(rawPath, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'image/*',
|
||||
'User-Agent': 'PortNimara-Client-Portal/1.0',
|
||||
'Cache-Control': 'no-cache'
|
||||
'Accept': 'image/*'
|
||||
},
|
||||
// Add timeout to prevent hanging
|
||||
signal: AbortSignal.timeout(45000) // 45 second timeout
|
||||
signal: AbortSignal.timeout(30000) // 30 second timeout
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[expenses/generate-pdf] Failed to fetch image from S3: ${response.status} ${response.statusText}`);
|
||||
console.error('[expenses/generate-pdf] Response headers:', Object.fromEntries(response.headers.entries()));
|
||||
|
||||
// Try with the original URL if encoding failed
|
||||
if (encodedUrl !== rawPath) {
|
||||
console.log('[expenses/generate-pdf] Retrying with original URL...');
|
||||
const originalResponse = await fetch(rawPath, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'image/*',
|
||||
'User-Agent': 'PortNimara-Client-Portal/1.0'
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
});
|
||||
|
||||
if (originalResponse.ok) {
|
||||
const arrayBuffer = await originalResponse.arrayBuffer();
|
||||
const imageBuffer = Buffer.from(arrayBuffer);
|
||||
console.log('[expenses/generate-pdf] Successfully fetched with original URL, Size:', imageBuffer.length);
|
||||
return imageBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -810,27 +776,13 @@ async function fetchReceiptImage(receipt: any): Promise<Buffer | null> {
|
|||
|
||||
} catch (fetchError: any) {
|
||||
console.error('[expenses/generate-pdf] Error fetching from S3 URL:', fetchError.message);
|
||||
console.error('[expenses/generate-pdf] Error details:', {
|
||||
name: fetchError.name,
|
||||
code: fetchError.code,
|
||||
message: fetchError.message
|
||||
});
|
||||
|
||||
// If it's a timeout or network error, try one more time with simpler approach
|
||||
if (fetchError.name === 'TimeoutError' || fetchError.name === 'AbortError' || fetchError.code === 'ECONNRESET') {
|
||||
console.log('[expenses/generate-pdf] Network error, trying simplified approach...');
|
||||
try {
|
||||
const simpleResponse = await fetch(rawPath, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(90000) // Extended timeout for final attempt
|
||||
});
|
||||
|
||||
if (simpleResponse.ok) {
|
||||
const arrayBuffer = await simpleResponse.arrayBuffer();
|
||||
const imageBuffer = Buffer.from(arrayBuffer);
|
||||
console.log('[expenses/generate-pdf] Successfully fetched image with simplified approach, Size:', imageBuffer.length);
|
||||
return imageBuffer;
|
||||
}
|
||||
} catch (finalError) {
|
||||
console.error('[expenses/generate-pdf] Final attempt also failed:', finalError);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't try multiple attempts for signed URLs as they may expire
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue