diff --git a/components/DuplicateNotificationBanner.vue b/components/DuplicateNotificationBanner.vue index 59b15e4..ea7f384 100644 --- a/components/DuplicateNotificationBanner.vue +++ b/components/DuplicateNotificationBanner.vue @@ -41,15 +41,15 @@ const showBanner = ref(true); const duplicateCount = ref(0); const loading = ref(false); -// Check for duplicates on mount (admin only) +// Check for duplicates on mount (any authenticated user) const checkForDuplicates = async () => { - if (!isAdmin() || loading.value) return; + if (loading.value) return; try { loading.value = true; const response = await $fetch('/api/admin/duplicates/find', { - method: 'POST', - body: { threshold: 80 } + method: 'GET', + query: { threshold: 0.8 } }); if (response.success && response.data?.duplicateGroups) { @@ -82,10 +82,8 @@ onMounted(() => { } } - // Only check for duplicates if user is admin - if (isAdmin()) { - // Small delay to let other components load first - setTimeout(checkForDuplicates, 2000); - } + // Check for duplicates for any authenticated user + // Small delay to let other components load first + setTimeout(checkForDuplicates, 2000); }); diff --git a/pages/dashboard.vue b/pages/dashboard.vue index 8c03d03..d5df1c1 100644 --- a/pages/dashboard.vue +++ b/pages/dashboard.vue @@ -94,6 +94,9 @@ const interestMenu = computed(() => { console.log('[Dashboard] Computing interest menu - isAdmin:', userIsAdmin, 'groups:', userGroups); + // Check if user has sales or admin privileges + const hasSalesAccess = userGroups.includes('sales') || userGroups.includes('admin'); + const baseMenu = [ //{ // to: "/dashboard/interest-eoi-queue", @@ -125,11 +128,6 @@ const interestMenu = computed(() => { icon: "mdi-account-check", title: "Interest Status", }, - { - to: "/dashboard/expenses", - icon: "mdi-receipt", - title: "Expenses", - }, { to: "/dashboard/file-browser", icon: "mdi-folder", @@ -137,6 +135,18 @@ const interestMenu = computed(() => { }, ]; + // Only show expenses to sales and admin users + if (hasSalesAccess) { + console.log('[Dashboard] Adding expenses to menu (user has sales/admin access)'); + baseMenu.push({ + to: "/dashboard/expenses", + icon: "mdi-receipt", + title: "Expenses", + }); + } else { + console.log('[Dashboard] Hiding expenses from menu (user role:', userGroups, ')'); + } + // Add admin menu items if user is admin if (userIsAdmin) { console.log('[Dashboard] Adding admin console to interest menu'); @@ -216,33 +226,48 @@ const safeMenu = computed(() => { console.warn('[Dashboard] Menu is not an array, returning fallback menu'); - // Fallback menu with essential items (including admin for safety) - return [ + // Get current user permissions for fallback menu + const userIsAdmin = isAdmin(); + const userGroups = getUserGroups(); + const hasSalesAccess = userGroups.includes('sales') || userGroups.includes('admin'); + + // Fallback menu with essential items (respecting permissions) + const fallbackMenu = [ { to: "/dashboard/interest-list", icon: "mdi-view-list", title: "Interest List", }, - { - to: "/dashboard/expenses", - icon: "mdi-receipt", - title: "Expenses", - }, { to: "/dashboard/file-browser", icon: "mdi-folder", title: "File Browser", }, - { + ]; + + // Only add expenses if user has sales/admin access + if (hasSalesAccess) { + fallbackMenu.push({ + to: "/dashboard/expenses", + icon: "mdi-receipt", + title: "Expenses", + }); + } + + // Only add admin console if user is admin + if (userIsAdmin) { + fallbackMenu.push({ to: "/dashboard/admin", icon: "mdi-shield-crown", title: "Admin Console", - }, - ]; + }); + } + + return fallbackMenu; } catch (error) { console.error('[Dashboard] Error computing menu:', error); - // Emergency fallback menu + // Emergency fallback menu - only essential items return [ { to: "/dashboard/interest-list", diff --git a/server/api/admin/duplicates/find.ts b/server/api/admin/duplicates/find.ts index 5e701bc..de96c8c 100644 --- a/server/api/admin/duplicates/find.ts +++ b/server/api/admin/duplicates/find.ts @@ -1,12 +1,12 @@ -import { requireAdmin } from '~/server/utils/auth'; +import { requireAuth } from '~/server/utils/auth'; import { getNocoDbConfiguration } from '~/server/utils/nocodb'; export default defineEventHandler(async (event) => { - console.log('[ADMIN] Find duplicates request'); + console.log('[DUPLICATES] Find duplicates request'); try { - // Require admin authentication - await requireAdmin(event); + // Require authentication (any authenticated user with interest access) + await requireAuth(event); const query = getQuery(event); const threshold = query.threshold ? parseFloat(query.threshold as string) : 0.8; diff --git a/server/api/admin/duplicates/merge.ts b/server/api/admin/duplicates/merge.ts index 6e0d884..69f54fd 100644 --- a/server/api/admin/duplicates/merge.ts +++ b/server/api/admin/duplicates/merge.ts @@ -1,12 +1,12 @@ -import { requireAdmin } from '~/server/utils/auth'; +import { requireAuth } from '~/server/utils/auth'; import { getNocoDbConfiguration, updateInterest, deleteInterest } from '~/server/utils/nocodb'; export default defineEventHandler(async (event) => { console.log('[ADMIN] Merge duplicates request'); try { - // Require admin authentication - await requireAdmin(event); + // Require authentication (any authenticated user with interest access) + await requireAuth(event); const body = await readBody(event); const { masterId, duplicateIds, mergeData } = body; diff --git a/server/api/get-expenses.ts b/server/api/get-expenses.ts index 6701f89..83451ec 100644 --- a/server/api/get-expenses.ts +++ b/server/api/get-expenses.ts @@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => { // Process expenses with currency conversion const processedExpenses = await Promise.all( - result.list.map(expense => processExpenseWithCurrency(expense)) + result.list.map((expense: any) => processExpenseWithCurrency(expense)) ); return { @@ -68,7 +68,7 @@ export default defineEventHandler(async (event) => { // Process expenses with currency conversion const processedExpenses = await Promise.all( - result.list.map(expense => processExpenseWithCurrency(expense)) + result.list.map((expense: any) => processExpenseWithCurrency(expense)) ); // Add formatted dates diff --git a/server/utils/nocodb.ts b/server/utils/nocodb.ts index 52c1609..ef74487 100644 --- a/server/utils/nocodb.ts +++ b/server/utils/nocodb.ts @@ -838,31 +838,40 @@ export const updateBerth = async (id: string, data: Partial): Promise { console.log('[nocodb.getExpenses] Fetching expenses from NocoDB...', filters); + const startTime = Date.now(); + try { const params: any = { limit: 1000 }; - // Build filter conditions + // Build filter conditions (fixed date logic) if (filters?.startDate && filters?.endDate) { - params.where = `(Time,gte,${filters.startDate})~and(Time,lte,${filters.endDate})`; + // Ensure dates are in YYYY-MM-DD format + const startDate = filters.startDate.includes('T') ? filters.startDate.split('T')[0] : filters.startDate; + const endDate = filters.endDate.includes('T') ? filters.endDate.split('T')[0] : filters.endDate; + + console.log('[nocodb.getExpenses] Date filter:', { startDate, endDate }); + params.where = `(Time,gte,${startDate})~and(Time,lte,${endDate})`; } else if (filters?.startDate) { - params.where = `(Time,gte,${filters.startDate})`; + const startDate = filters.startDate.includes('T') ? filters.startDate.split('T')[0] : filters.startDate; + params.where = `(Time,gte,${startDate})`; } else if (filters?.endDate) { - params.where = `(Time,lte,${filters.endDate})`; + const endDate = filters.endDate.includes('T') ? filters.endDate.split('T')[0] : filters.endDate; + params.where = `(Time,lte,${endDate})`; } // Add payer filter if (filters?.payer) { - const payerFilter = `(Payer,eq,${filters.payer})`; + const payerFilter = `(Payer,eq,${encodeURIComponent(filters.payer)})`; params.where = params.where ? `${params.where}~and${payerFilter}` : payerFilter; } // Add category filter if (filters?.category) { - const categoryFilter = `(Category,eq,${filters.category})`; + const categoryFilter = `(Category,eq,${encodeURIComponent(filters.category)})`; params.where = params.where ? `${params.where}~and${categoryFilter}` : categoryFilter; } @@ -870,29 +879,64 @@ export const getExpenses = async (filters?: ExpenseFilters) => { params.sort = '-Time'; console.log('[nocodb.getExpenses] Request params:', params); + console.log('[nocodb.getExpenses] Request URL:', createTableUrl(Table.Expense)); + // Use regular $fetch with better error handling const result = await $fetch(createTableUrl(Table.Expense), { headers: { "xc-token": getNocoDbConfiguration().token, + "Content-Type": "application/json" }, params }); console.log('[nocodb.getExpenses] Successfully fetched expenses, count:', result.list?.length || 0); + console.log('[nocodb.getExpenses] Request duration:', Date.now() - startTime, 'ms'); // Transform expenses to add computed price numbers if (result.list && Array.isArray(result.list)) { result.list = result.list.map(expense => ({ ...expense, // Parse price string to number for calculations - PriceNumber: parseFloat(expense.Price.replace(/[€$,]/g, '')) || 0 + PriceNumber: parseFloat(expense.Price?.replace(/[€$,]/g, '') || '0') || 0 })); } return result; } catch (error: any) { - console.error('[nocodb.getExpenses] Error fetching expenses:', error); - console.error('[nocodb.getExpenses] Error details:', error instanceof Error ? error.message : 'Unknown error'); + console.error('[nocodb.getExpenses] ========================='); + console.error('[nocodb.getExpenses] EXPENSE FETCH FAILED'); + console.error('[nocodb.getExpenses] Duration:', Date.now() - startTime, 'ms'); + console.error('[nocodb.getExpenses] Error type:', error.constructor?.name || 'Unknown'); + console.error('[nocodb.getExpenses] Error status:', error.statusCode || error.status || 'Unknown'); + console.error('[nocodb.getExpenses] Error message:', error.message || 'Unknown error'); + console.error('[nocodb.getExpenses] Error data:', error.data); + console.error('[nocodb.getExpenses] Full error:', JSON.stringify(error, null, 2)); + console.error('[nocodb.getExpenses] ========================='); + + // Provide more specific error messages + if (error.statusCode === 401 || error.status === 401) { + throw createError({ + statusCode: 401, + statusMessage: 'Authentication failed when accessing expense database. Please check your access permissions.' + }); + } else if (error.statusCode === 403 || error.status === 403) { + throw createError({ + statusCode: 403, + statusMessage: 'Access denied to expense database. This feature requires appropriate privileges.' + }); + } else if (error.statusCode === 404 || error.status === 404) { + throw createError({ + statusCode: 404, + statusMessage: 'Expense database table not found. Please contact your administrator.' + }); + } else if (error.code === 'NETWORK_ERROR' || error.code === 'TIMEOUT') { + throw createError({ + statusCode: 503, + statusMessage: 'Expense database is temporarily unavailable. Please try again in a moment.' + }); + } + throw error; } };