Replace all mock/placeholder data with real data systems
All checks were successful
Build And Push Image / docker (push) Successful in 2m13s

- Added getUserCount() method to Keycloak admin for real user statistics
- Replaced hardcoded userCount (25) with live Keycloak data in admin stats
- Fixed board meeting API to query real events, removed Jan 15 2025 fallback
- Updated board stats to count real events instead of hardcoded 3
- Created member-tiers service for proper tier determination
- Created dues-calculator service for accurate dues tracking
- Updated auth callback to use member-tiers service
- Updated overdue-count API to use dues-calculator
- Added data quality tracking with confidence levels
- Added proper error handling - returns null/0 instead of fake data
- Included source tracking for all data (live/calculated/fallback)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-31 18:28:38 +02:00
parent 9d93f0ca84
commit 1aef356d78
8 changed files with 954 additions and 121 deletions

View File

@@ -1,5 +1,5 @@
export default defineEventHandler(async (event) => {
console.log('📊 Simple admin stats requested at:', new Date().toISOString());
console.log('📊 Admin stats requested at:', new Date().toISOString());
try {
// Check if user is admin
@@ -17,17 +17,34 @@ export default defineEventHandler(async (event) => {
console.log('✅ Admin access verified for user:', session.user.email);
// Return simple user-focused stats without system metrics
// Get real user count from Keycloak
let userCount = 0;
try {
const { createKeycloakAdminClient } = await import('~/server/utils/keycloak-admin');
const keycloakAdmin = createKeycloakAdminClient();
userCount = await keycloakAdmin.getUserCount(false); // Exclude service accounts
console.log('✅ Retrieved real user count from Keycloak:', userCount);
} catch (keycloakError: any) {
console.error('⚠️ Failed to get user count from Keycloak:', keycloakError);
// Instead of returning mock data, return 0 or null to indicate data unavailable
console.log('⚠️ User count unavailable, returning 0');
userCount = 0;
}
// Return stats with real data
const stats = {
// Simple user count (mock data for now - would come from Keycloak API)
userCount: 25,
// Real user count from Keycloak (0 if unavailable)
userCount: userCount,
// Basic portal health without system metrics
// Basic portal health
portalStatus: 'online',
lastUpdate: new Date().toISOString()
lastUpdate: new Date().toISOString(),
// Add flag to indicate if data is from cache or live
dataSource: userCount > 0 ? 'live' : 'unavailable'
};
console.log('✅ Simple admin stats retrieved successfully');
console.log('✅ Admin stats retrieved successfully:', stats);
return stats;
} catch (error: any) {

View File

@@ -36,12 +36,24 @@ export default defineEventHandler(async (event) => {
// Get user info
const userInfo = await keycloak.getUserInfo(tokens.access_token);
// Tier determination logic - admin > board > user priority
const determineTier = (groups: string[]): 'user' | 'board' | 'admin' => {
if (groups.includes('admin')) return 'admin';
if (groups.includes('board')) return 'board';
return 'user'; // Default tier
};
// Use member-tiers service for proper tier determination
const { determineMemberTierByEmail, determineMemberTierFromKeycloak } = await import('~/server/utils/member-tiers');
// First try to determine tier by email (checks NocoDB and Keycloak)
const tierResult = await determineMemberTierByEmail(userInfo.email);
// Log tier determination for monitoring
console.log(`[auth-callback] Tier determination for ${userInfo.email}:`, {
tier: tierResult.tier,
source: tierResult.source,
confidence: tierResult.confidence,
reason: tierResult.reason
});
// Alert admin if tier confidence is low
if (tierResult.confidence === 'low') {
console.warn(`[auth-callback] Low confidence tier assignment for ${userInfo.email}. Manual review recommended.`);
}
// Create session
const sessionData = {
@@ -52,7 +64,8 @@ export default defineEventHandler(async (event) => {
firstName: userInfo.given_name,
lastName: userInfo.family_name,
username: userInfo.preferred_username,
tier: determineTier(userInfo.groups || []),
tier: tierResult.tier,
tierSource: tierResult.source, // Track where tier came from
groups: userInfo.groups || ['user'],
},
tokens: {

View File

@@ -5,14 +5,12 @@ export default defineEventHandler(async (event) => {
// Try to get next meeting from events
const eventsClient = createNocoDBEventsClient();
const now = new Date();
const thirtyDaysFromNow = new Date();
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
let nextMeeting = null;
try {
const eventsResponse = await eventsClient.findAll({
limit: 50
limit: 100 // Increased limit to ensure we get all upcoming events
});
// Handle different possible response structures
@@ -24,8 +22,11 @@ export default defineEventHandler(async (event) => {
.filter((event: any) => {
if (!event.start_datetime) return false;
const eventDate = new Date(event.start_datetime);
// Check if event is in the future and is a meeting
return eventDate >= now &&
(event.event_type === 'meeting' || event.title?.toLowerCase().includes('meeting'));
(event.event_type === 'meeting' ||
event.title?.toLowerCase().includes('meeting') ||
event.title?.toLowerCase().includes('board'));
})
.sort((a: any, b: any) => new Date(a.start_datetime).getTime() - new Date(b.start_datetime).getTime());
@@ -46,8 +47,12 @@ export default defineEventHandler(async (event) => {
minute: '2-digit',
timeZoneName: 'short'
}),
location: meeting.location,
description: meeting.description
location: meeting.location || 'To be announced',
description: meeting.description || '',
// Add additional fields for better UI
isoDate: meeting.start_datetime,
endTime: meeting.end_datetime || null,
eventType: meeting.event_type || 'meeting'
};
}
}
@@ -55,37 +60,34 @@ export default defineEventHandler(async (event) => {
console.error('[next-meeting] Error fetching events:', error);
}
// Fallback if no meetings found
if (!nextMeeting) {
nextMeeting = {
id: null,
title: 'Board Meeting',
date: 'January 15, 2025',
time: '7:00 PM EST',
location: 'TBD',
description: 'Monthly board meeting'
// Return appropriate response based on whether we found a meeting
if (nextMeeting) {
console.log('[next-meeting] Found upcoming meeting:', nextMeeting.title);
return {
success: true,
data: nextMeeting,
source: 'live'
};
} else {
// No meetings found - return null instead of fake data
console.log('[next-meeting] No upcoming meetings found');
return {
success: true,
data: null,
message: 'No upcoming meetings scheduled',
source: 'live'
};
}
return {
success: true,
data: nextMeeting
};
} catch (error: any) {
console.error('[next-meeting] Error:', error);
// Return fallback data
// Return error response instead of fallback data
return {
success: true,
data: {
id: null,
title: 'Board Meeting',
date: 'January 15, 2025',
time: '7:00 PM EST',
location: 'TBD',
description: 'Monthly board meeting'
}
success: false,
data: null,
error: 'Unable to fetch meeting information',
message: 'Please check back later or contact administrator'
};
}
});

View File

@@ -13,7 +13,8 @@ export default defineEventHandler(async (event) => {
totalMembers: 0,
activeMembers: 0,
upcomingMeetings: 0,
pendingActions: 0
pendingActions: 0,
dataSource: 'unavailable'
}
};
}
@@ -23,10 +24,10 @@ export default defineEventHandler(async (event) => {
member.membership_status === 'Active'
).length;
// Get upcoming meetings count - simplified approach since events API might still have issues
let upcomingMeetings = 3; // Default fallback
// Get real upcoming meetings count from events
let upcomingMeetings = 0;
let eventsSource = 'unavailable';
// Try to get real events data but don't fail if it's not working
try {
const eventsClient = createNocoDBEventsClient();
const now = new Date();
@@ -34,32 +35,43 @@ export default defineEventHandler(async (event) => {
thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30);
const eventsResponse = await eventsClient.findAll({
limit: 100
limit: 200 // Increased limit to get all events
});
// Handle different possible response structures
const eventsList = (eventsResponse as any)?.list || [];
if (eventsList && Array.isArray(eventsList)) {
// Count meetings in the next 30 days
upcomingMeetings = eventsList.filter((event: any) => {
if (!event.start_datetime) return false;
const eventDate = new Date(event.start_datetime);
return eventDate >= now &&
eventDate <= thirtyDaysFromNow &&
(event.event_type === 'meeting' || event.title?.toLowerCase().includes('meeting'));
(event.event_type === 'meeting' ||
event.title?.toLowerCase().includes('meeting') ||
event.title?.toLowerCase().includes('board'));
}).length;
eventsSource = 'live';
console.log(`[board-stats] Found ${upcomingMeetings} upcoming meetings from live data`);
}
} catch (error) {
console.error('[board-stats] Error fetching events, using fallback:', error);
// Keep the fallback value of 3
console.error('[board-stats] Error fetching events:', error);
// Keep upcomingMeetings as 0 instead of using fallback
upcomingMeetings = 0;
eventsSource = 'error';
}
// Get overdue dues count for pending actions
let pendingActions = 0;
let duesSource = 'unavailable';
try {
const overdueResponse: any = await $fetch('/api/members/overdue-count');
pendingActions = overdueResponse?.data?.count || 0;
duesSource = 'live';
} catch (error) {
console.error('[board-stats] Error fetching overdue count:', error);
pendingActions = 0;
duesSource = 'error';
}
return {
@@ -68,7 +80,13 @@ export default defineEventHandler(async (event) => {
totalMembers,
activeMembers,
upcomingMeetings,
pendingActions
pendingActions,
// Add metadata about data sources
dataSources: {
members: 'live',
events: eventsSource,
dues: duesSource
}
}
};

View File

@@ -1,38 +1,9 @@
// server/api/members/overdue-count.get.ts
// Helper function to calculate overdue duration
function calculateOverdueDuration(dueDate: Date, today: Date): {
years: number;
months: number;
totalMonths: number;
formattedDuration: string;
} {
const diffTime = today.getTime() - dueDate.getTime();
const diffMonths = Math.floor(diffTime / (1000 * 60 * 60 * 24 * 30.44)); // Average days per month
const years = Math.floor(diffMonths / 12);
const months = diffMonths % 12;
let formattedDuration = '';
if (years > 0) {
formattedDuration += `${years} year${years !== 1 ? 's' : ''}`;
if (months > 0) {
formattedDuration += ` ${months} month${months !== 1 ? 's' : ''}`;
}
} else {
formattedDuration = `${diffMonths} month${diffMonths !== 1 ? 's' : ''}`;
}
return {
years,
months,
totalMonths: diffMonths,
formattedDuration: `${formattedDuration} overdue`
};
}
export default defineEventHandler(async (event) => {
try {
const { getMembers } = await import('~/server/utils/nocodb');
const { calculateDuesStatus } = await import('~/server/utils/dues-calculator');
// Get all members
const allMembers = await getMembers();
@@ -42,7 +13,8 @@ export default defineEventHandler(async (event) => {
success: true,
data: {
count: 0,
overdueMembers: []
overdueMembers: [],
dataSource: 'unavailable'
}
};
}
@@ -51,63 +23,77 @@ export default defineEventHandler(async (event) => {
const overdueMembers: any[] = [];
for (const member of allMembers.list) {
// Check for severely overdue members (more than 1 year past due date)
let overdueDuration = null;
// Use dues-calculator for proper dues status calculation
const duesStatus = await calculateDuesStatus(member);
if (member.current_year_dues_paid === 'true' && member.membership_date_paid) {
// If dues are marked as paid, check if it's been more than 1 year since payment
const lastPaidDate = new Date(member.membership_date_paid);
const oneYearFromPayment = new Date(lastPaidDate);
oneYearFromPayment.setFullYear(oneYearFromPayment.getFullYear() + 1);
// Only include members who are overdue
if (duesStatus.isOverdue && duesStatus.daysOverdue !== null) {
// Format overdue duration
const totalMonths = Math.floor(duesStatus.daysOverdue / 30.44);
const years = Math.floor(totalMonths / 12);
const months = totalMonths % 12;
if (today > oneYearFromPayment) {
overdueDuration = calculateOverdueDuration(oneYearFromPayment, today);
}
} else if (member.current_year_dues_paid !== 'true') {
// If dues are not paid, check payment due date or member since date
let dueDate: Date;
if (member.payment_due_date) {
dueDate = new Date(member.payment_due_date);
} else if (member.member_since) {
// Fallback: 1 year from member since date
dueDate = new Date(member.member_since);
dueDate.setFullYear(dueDate.getFullYear() + 1);
let formattedDuration = '';
if (years > 0) {
formattedDuration = `${years} year${years !== 1 ? 's' : ''}`;
if (months > 0) {
formattedDuration += ` ${months} month${months !== 1 ? 's' : ''}`;
}
} else if (totalMonths > 0) {
formattedDuration = `${totalMonths} month${totalMonths !== 1 ? 's' : ''}`;
} else {
// Skip if we can't determine due date
continue;
const days = duesStatus.daysOverdue;
formattedDuration = `${days} day${days !== 1 ? 's' : ''}`;
}
formattedDuration += ' overdue';
// Check if more than 1 year overdue
const oneYearOverdue = new Date(dueDate);
oneYearOverdue.setFullYear(oneYearOverdue.getFullYear() + 1);
if (today > oneYearOverdue) {
overdueDuration = calculateOverdueDuration(oneYearOverdue, today);
}
}
if (overdueDuration) {
overdueMembers.push({
id: member.Id,
name: member.FullName || `${member.first_name} ${member.last_name}`,
email: member.email,
status: member.membership_status,
overdueDuration: overdueDuration.formattedDuration,
totalMonthsOverdue: overdueDuration.totalMonths,
overdueDuration: formattedDuration,
daysOverdue: duesStatus.daysOverdue,
totalMonthsOverdue: totalMonths,
dueDate: duesStatus.dueDate,
amount: duesStatus.amount,
currency: duesStatus.currency,
dataSource: duesStatus.source,
confidence: duesStatus.confidence,
isInactive: member.membership_status === 'Inactive'
});
}
}
// Sort by most overdue first
overdueMembers.sort((a, b) => b.totalMonthsOverdue - a.totalMonthsOverdue);
// Sort by most overdue first (by days)
overdueMembers.sort((a, b) => b.daysOverdue - a.daysOverdue);
// Log data quality metrics
const highConfidenceCount = overdueMembers.filter(m => m.confidence === 'high').length;
const mediumConfidenceCount = overdueMembers.filter(m => m.confidence === 'medium').length;
const lowConfidenceCount = overdueMembers.filter(m => m.confidence === 'low').length;
console.log(`[overdue-count] Found ${overdueMembers.length} overdue members:`, {
highConfidence: highConfidenceCount,
mediumConfidence: mediumConfidenceCount,
lowConfidence: lowConfidenceCount
});
// Alert if many low confidence results
if (lowConfidenceCount > overdueMembers.length * 0.3) {
console.warn(`[overdue-count] High percentage of low confidence dues data (${lowConfidenceCount}/${overdueMembers.length}). Data quality review recommended.`);
}
return {
success: true,
data: {
count: overdueMembers.length,
overdueMembers: overdueMembers
overdueMembers: overdueMembers,
dataQuality: {
highConfidence: highConfidenceCount,
mediumConfidence: mediumConfidenceCount,
lowConfidence: lowConfidenceCount
}
}
};