Implementation Into Wix
/**
* Enhanced Volunteer Time Daemon v2.0 - Wix Velo Implementation
* Network Theory Applied Research Institute, Inc.
*
* Backend implementation for Wix Velo with scheduled analysis,
* database storage, and automated notifications.
*/
// ============================================================================
// BACKEND: volunteerTimeAnalysis.js
// ============================================================================
import { fetch } from 'wix-fetch';
import wixData from 'wix-data';
import { scheduleJob } from 'wix-crm-backend';
import { emailContact } from 'wix-crm-backend';
/**
* Main Volunteer Time Analysis Class for Wix Velo
*/
export class VolunteerTimeDaemon {
constructor() {
this.dataUrl = 'https://raw.githubusercontent.com/NetworkTheoryAppliedResearchInstitute/records/refs/heads/main/volunteertime';
// Event type classifications
this.EVENT_TYPES = {
AUTO_TIMESTAMP: 'auto_ts',
EXPLICIT_CLOCKIN: 'clock_in',
EXPLICIT_CLOCKOUT: 'clock_out',
BREAK_START: 'break_start',
BREAK_END: 'break_end',
SESSION_NOTE: 'note',
SYSTEM_MARKER: 'system'
};
// Priority weights (highest to lowest)
this.PRIORITY_WEIGHTS = {
explicit_clockout: 100,
explicit_clockin: 95,
break_markers: 90,
system_markers: 50,
auto_timestamp: 10
};
// Validation thresholds
this.VALIDATION_THRESHOLDS = {
EXTREME_SHORT: 5 * 60, // 5 minutes
SUSPICIOUS_SHORT: 30 * 60, // 30 minutes
NORMAL_MIN: 30 * 60, // 30 minutes
NORMAL_MAX: 8 * 60 * 60, // 8 hours
CONCERNING_LONG: 12 * 60 * 60, // 12 hours
EXTREME_LONG: 20 * 60 * 60, // 20 hours
WEEKLY_CONCERN: 40 * 60 * 60, // 40 hours/week
WEEKLY_EXTREME: 60 * 60 * 60 // 60 hours/week
};
// Clock out detection patterns
this.CLOCKOUT_PATTERNS = [
/Clock Out @ (\d{1,2}:\d{2} (?:AM|PM) \w+)/i,
/clocked out at (\d{1,2}:\d{2}(?:\s*(?:AM|PM))?)/i,
/end session (\d{1,2}:\d{2}(?:\s*(?:AM|PM))?)/i,
/stopping work @ (\d{1,2}:\d{2}(?:\s*(?:AM|PM))?)/i,
/session complete (\d{1,2}:\d{2}(?:\s*(?:AM|PM))?)/i,
/finished @ (\d{1,2}:\d{2}(?:\s*(?:AM|PM))?)/i
];
// User name mappings (expand as needed)
this.userNames = {
'3f573d1-7315-4212-bd3c-8a301b92015f': 'HLine User',
// Add more mappings from member database
};
}
/**
* Run complete analysis workflow (called by scheduled job)
*/
async runAnalysis() {
try {
console.log('🔄 Starting volunteer time analysis...');
// Fetch and process data
const rawData = await this.fetchVolunteerData();
const processedData = this.processRawData(rawData);
const last7DaysData = this.filterLast7Days(processedData);
// Generate analysis
const hoursSummary = this.generateHoursSummary(last7DaysData);
const validationReport = this.generateValidationReport(hoursSummary);
// Store results in Wix Data
const analysisResult = await this.storeAnalysisResults(hoursSummary, validationReport);
// Generate and store individual user reports
await this.generateAndStoreUserReports(hoursSummary);
// Send notifications for critical issues
await this.handleNotifications(validationReport);
console.log('✅ Analysis complete, stored with ID:', analysisResult._id);
return analysisResult;
} catch (error) {
console.error('❌ Analysis failed:', error);
await this.logError(error);
throw error;
}
}
/**
* Fetch volunteer time data from GitHub
*/
async fetchVolunteerData() {
try {
const response = await fetch(this.dataUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.text();
} catch (error) {
throw new Error(`Failed to fetch volunteer data: ${error.message}`);
}
}
/**
* Process raw CSV/text data into structured format
*/
processRawData(rawText) {
const lines = rawText.split('\n').filter(line => line.trim());
const entries = [];
for (const line of lines) {
try {
const entry = this.parseLine(line);
if (entry) {
entries.push(entry);
}
} catch (error) {
console.warn('⚠️ Failed to parse line:', line.substring(0, 100) + '...');
}
}
return this.groupIntoSessions(entries);
}
/**
* Parse individual line into structured entry
*/
parseLine(line) {
const hlineMatch = line.match(/HLine([a-f0-9-]+)(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\*\*(.*?)\*\*/);
if (hlineMatch) {
const [, userId, timestamp, content] = hlineMatch;
return {
userId: userId,
timestamp: timestamp,
content: content,
rawLine: line,
eventType: this.determineEventType(content),
clockOutData: this.extractClockOutTime(content),
parsedDate: new Date(timestamp)
};
}
return null;
}
/**
* Determine event type from content
*/
determineEventType(content) {
if (this.extractClockOutTime(content)) {
return this.EVENT_TYPES.EXPLICIT_CLOCKOUT;
}
if (content.toLowerCase().includes('clock in') ||
content.toLowerCase().includes('starting') ||
content.toLowerCase().includes('begin session')) {
return this.EVENT_TYPES.EXPLICIT_CLOCKIN;
}
if (content.toLowerCase().includes('break') ||
content.toLowerCase().includes('pause')) {
return this.EVENT_TYPES.BREAK_START;
}
return this.EVENT_TYPES.AUTO_TIMESTAMP;
}
/**
* Extract explicit clock out time from content
*/
extractClockOutTime(content) {
for (const pattern of this.CLOCKOUT_PATTERNS) {
const match = content.match(pattern);
if (match) {
return {
eventType: 'explicit_clockout',
localTime: match[1],
priority: 'explicit',
extractedFrom: match[0],
rawMatch: match[1]
};
}
}
return null;
}
/**
* Group entries into work sessions
*/
groupIntoSessions(entries) {
const sessions = [];
const userSessions = {};
// Group by user
for (const entry of entries) {
if (!userSessions[entry.userId]) {
userSessions[entry.userId] = [];
}
userSessions[entry.userId].push(entry);
}
// Create sessions for each user
for (const [userId, userEntries] of Object.entries(userSessions)) {
userEntries.sort((a, b) => a.parsedDate - b.parsedDate);
let currentSession = null;
for (const entry of userEntries) {
if (!currentSession ||
(entry.parsedDate - currentSession.lastActivity) > (2 * 60 * 60 * 1000)) {
currentSession = {
sessionId: `session_${userId}_${entry.timestamp}`,
userId: userId,
startEvent: entry,
endEvent: entry,
entries: [entry],
lastActivity: entry.parsedDate,
flags: []
};
sessions.push(currentSession);
} else {
currentSession.entries.push(entry);
currentSession.endEvent = entry;
currentSession.lastActivity = entry.parsedDate;
}
}
}
return sessions;
}
/**
* Filter sessions to last 7 days
*/
filterLast7Days(sessions) {
const sevenDaysAgo = new Date(Date.now() - (7 * 24 * 60 * 60 * 1000));
return sessions.filter(session => session.startEvent.parsedDate >= sevenDaysAgo);
}
/**
* Calculate session duration using priority system
*/
calculateSessionDuration(session) {
const endEvent = this.selectHighestPriorityEndEvent(session);
const startEvent = session.startEvent;
let startTime = startEvent.parsedDate;
let endTime;
if (endEvent.clockOutData && endEvent.clockOutData.priority === 'explicit') {
endTime = this.parseExplicitTime(endEvent.clockOutData.localTime, endEvent.parsedDate);
} else {
endTime = endEvent.parsedDate;
}
const durationMs = endTime - startTime;
const durationSeconds = Math.floor(durationMs / 1000);
const hours = durationSeconds / 3600;
return {
startTime: startTime,
endTime: endTime,
durationMs: durationMs,
durationSeconds: durationSeconds,
hours: hours,
endEventType: endEvent.eventType,
endEventPriority: endEvent.clockOutData?.priority || 'auto'
};
}
/**
* Select highest priority end event from session
*/
selectHighestPriorityEndEvent(session) {
let highestPriority = -1;
let selectedEvent = session.endEvent;
for (const entry of session.entries) {
const priority = this.PRIORITY_WEIGHTS[entry.eventType] || 0;
if (entry.clockOutData) {
const boostedPriority = this.PRIORITY_WEIGHTS.explicit_clockout;
if (boostedPriority > highestPriority) {
highestPriority = boostedPriority;
selectedEvent = entry;
}
} else if (priority > highestPriority) {
highestPriority = priority;
selectedEvent = entry;
}
}
return selectedEvent;
}
/**
* Parse explicit time like "10:30 PM ET"
*/
parseExplicitTime(timeString, contextDate) {
try {
const timeMatch = timeString.match(/(\d{1,2}):(\d{2})\s*(AM|PM)?/i);
if (!timeMatch) {
return contextDate;
}
let hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
const isPM = timeMatch[3] && timeMatch[3].toUpperCase() === 'PM';
if (isPM && hours !== 12) {
hours += 12;
} else if (!isPM && hours === 12) {
hours = 0;
}
const newDate = new Date(contextDate);
newDate.setHours(hours, minutes, 0, 0);
if (newDate < contextDate) {
newDate.setDate(newDate.getDate() + 1);
}
return newDate;
} catch (error) {
console.warn('Failed to parse explicit time:', timeString);
return contextDate;
}
}
/**
* Generate hours summary for period
*/
generateHoursSummary(sessions) {
const userMap = {};
for (const session of sessions) {
if (!userMap[session.userId]) {
userMap[session.userId] = {
userId: session.userId,
userName: this.getUserName(session.userId),
sessions: [],
totalHours: 0,
sessionCount: 0,
longestSession: 0,
longestSessionDetails: null,
flags: [],
validationIssues: []
};
}
const user = userMap[session.userId];
const validation = this.validateSession(session);
user.sessions.push({
sessionId: session.sessionId,
start: session.startEvent.timestamp,
end: session.endEvent.timestamp,
duration: validation.duration,
validation: validation
});
user.totalHours += validation.duration.hours;
user.sessionCount++;
user.flags.push(...validation.flags);
user.validationIssues.push(...validation.errors);
if (validation.duration.hours > user.longestSession) {
user.longestSession = validation.duration.hours;
user.longestSessionDetails = {
sessionId: session.sessionId,
duration: validation.duration,
validation: validation
};
}
}
for (const user of Object.values(userMap)) {
user.averageSessionLength = user.sessionCount > 0 ? user.totalHours / user.sessionCount : 0;
}
return {
periodStart: new Date(Date.now() - (7 * 24 * 60 * 60 * 1000)).toISOString(),
periodEnd: new Date().toISOString(),
totalSessions: sessions.length,
users: Object.values(userMap)
};
}
/**
* Validate session for concerning patterns
*/
validateSession(session) {
const duration = this.calculateSessionDuration(session);
const flags = [];
const warnings = [];
const errors = [];
if (duration.durationSeconds < this.VALIDATION_THRESHOLDS.EXTREME_SHORT) {
flags.push('EXTREME_SHORT');
} else if (duration.durationSeconds < this.VALIDATION_THRESHOLDS.SUSPICIOUS_SHORT) {
flags.push('SUSPICIOUS_SHORT');
} else if (duration.durationSeconds > this.VALIDATION_THRESHOLDS.EXTREME_LONG) {
flags.push('EXTREME_LENGTH');
warnings.push(`Session length of ${duration.hours.toFixed(1)}h exceeds 20h threshold`);
} else if (duration.durationSeconds > this.VALIDATION_THRESHOLDS.CONCERNING_LONG) {
flags.push('CONCERNING_LENGTH');
warnings.push(`Session length of ${duration.hours.toFixed(1)}h exceeds 12h threshold`);
}
if (duration.endTime < duration.startTime) {
errors.push('END_BEFORE_START');
}
if (duration.endEventPriority === 'explicit') {
flags.push('EXPLICIT_CLOCKOUT_USED');
}
return {
isValid: errors.length === 0,
flags: flags,
warnings: warnings,
errors: errors,
duration: duration
};
}
/**
* Generate validation report
*/
generateValidationReport(summary) {
const report = {
overallHealth: 'GOOD',
criticalIssues: [],
warnings: [],
recommendations: [],
statistics: {
totalUsers: summary.users.length,
totalHours: summary.users.reduce((sum, u) => sum + u.totalHours, 0),
extremeSessions: 0,
longSessions: 0,
validationErrors: 0
}
};
for (const user of summary.users) {
if (user.longestSession > 20) {
report.criticalIssues.push({
type: 'EXTREME_SESSION_LENGTH',
user: user.userName,
userId: user.userId,
sessionLength: user.longestSession.toFixed(1),
endType: user.longestSessionDetails.validation.duration.endEventType,
priority: user.longestSessionDetails.validation.duration.endEventPriority,
message: `${user.userName} logged ${user.longestSession.toFixed(1)}h session`
});
report.statistics.extremeSessions++;
}
if (user.longestSession > 12 && user.longestSession <= 20) {
report.statistics.longSessions++;
}
if (user.totalHours > 60) {
report.criticalIssues.push({
type: 'EXCESSIVE_WEEKLY_HOURS',
user: user.userName,
userId: user.userId,
totalHours: user.totalHours.toFixed(1),
message: `${user.userName} logged ${user.totalHours.toFixed(1)}h in 7 days`
});
} else if (user.totalHours > 40) {
report.warnings.push({
type: 'HIGH_WEEKLY_HOURS',
user: user.userName,
userId: user.userId,
totalHours: user.totalHours.toFixed(1),
message: `${user.userName} logged ${user.totalHours.toFixed(1)}h in 7 days`
});
}
if (user.validationIssues.length > 0) {
report.warnings.push({
type: 'DATA_VALIDATION_ISSUES',
user: user.userName,
userId: user.userId,
issues: user.validationIssues
});
report.statistics.validationErrors += user.validationIssues.length;
}
}
if (report.criticalIssues.length > 0) {
report.overallHealth = 'CRITICAL';
} else if (report.warnings.length > 0) {
report.overallHealth = 'WARNING';
}
return report;
}
/**
* Get user display name from userId
*/
getUserName(userId) {
return this.userNames[userId] || `User-${userId.substring(0, 8)}`;
}
/**
* Store analysis results in Wix Data
*/
async storeAnalysisResults(summary, validation) {
const analysisData = {
periodStart: summary.periodStart,
periodEnd: summary.periodEnd,
overallHealth: validation.overallHealth,
totalUsers: validation.statistics.totalUsers,
totalHours: validation.statistics.totalHours,
extremeSessions: validation.statistics.extremeSessions,
longSessions: validation.statistics.longSessions,
validationErrors: validation.statistics.validationErrors,
criticalIssues: validation.criticalIssues,
warnings: validation.warnings,
summary: summary,
validation: validation,
generatedAt: new Date()
};
return await wixData.save("VolunteerAnalysis", analysisData);
}
/**
* Generate and store individual user reports
*/
async generateAndStoreUserReports(summary) {
const reports = [];
for (const user of summary.users) {
const reportContent = this.buildUserReportContent(user, summary);
const userHealth = this.calculateUserHealth(user);
const reportData = {
userId: user.userId,
userName: user.userName,
periodStart: summary.periodStart,
periodEnd: summary.periodEnd,
healthStatus: userHealth.status,
totalHours: user.totalHours,
sessionCount: user.sessionCount,
averageSessionLength: user.averageSessionLength,
longestSession: user.longestSession,
reportContent: reportContent,
criticalIssues: userHealth.issues.filter(i => i.type.includes('EXTREME') || i.type.includes('EXCESSIVE')),
warnings: userHealth.issues.filter(i => !i.type.includes('EXTREME') && !i.type.includes('EXCESSIVE')),
flags: userHealth.flags,
generatedAt: new Date()
};
const savedReport = await wixData.save("UserReports", reportData);
reports.push(savedReport);
}
return reports;
}
/**
* Calculate user health status
*/
calculateUserHealth(user) {
const issues = [];
const flags = [...new Set(user.flags)];
if (user.longestSession > 20) {
issues.push({
type: 'EXTREME_SESSION',
message: `Longest session of ${user.longestSession.toFixed(1)}h exceeds healthy limits`
});
}
if (user.totalHours > 60) {
issues.push({
type: 'EXCESSIVE_HOURS',
message: `Total of ${user.totalHours.toFixed(1)}h exceeds 60h weekly threshold`
});
} else if (user.totalHours > 40) {
issues.push({
type: 'HIGH_HOURS',
message: `Total of ${user.totalHours.toFixed(1)}h exceeds 40h weekly threshold`
});
}
if (user.averageSessionLength > 12) {
issues.push({
type: 'LONG_AVERAGE_SESSIONS',
message: `Average session of ${user.averageSessionLength.toFixed(1)}h may indicate fatigue risk`
});
}
let status = '🟢 HEALTHY';
if (issues.some(i => i.type.includes('EXTREME') || i.type.includes('EXCESSIVE'))) {
status = '🔴 CRITICAL';
} else if (issues.length > 0) {
status = '🟡 WARNING';
}
return { status, issues, flags };
}
/**
* Build comprehensive user report content
*/
buildUserReportContent(user, summary) {
const periodStart = new Date(summary.periodStart);
const periodEnd = new Date(summary.periodEnd);
const userHealth = this.calculateUserHealth(user);
return `# Volunteer Time Report: ${user.userName}
**Analysis Period:** ${periodStart.toLocaleDateString()} - ${periodEnd.toLocaleDateString()}
**Report Generated:** ${new Date().toLocaleString()}
**System Health Status:** ${userHealth.status}
## 📊 EXECUTIVE SUMMARY
| Metric | Value | Status |
|--------|-------|--------|
| **Total Hours** | ${user.totalHours.toFixed(1)}h | ${this.getHoursStatus(user.totalHours)} |
| **Sessions** | ${user.sessionCount} | 🟢 Normal |
| **Average Session** | ${user.averageSessionLength.toFixed(1)}h | ${this.getAvgSessionStatus(user.averageSessionLength)} |
| **Longest Session** | ${user.longestSession.toFixed(1)}h | ${this.getLongestSessionStatus(user.longestSession)} |
| **Validation Issues** | ${user.validationIssues.length} | ${user.validationIssues.length === 0 ? '✅ Clean' : '⚠️ Needs Review'} |
## 🏥 HEALTH ASSESSMENT
### Overall Status: ${userHealth.status}
${userHealth.issues.length > 0 ? `
#### Issues Identified:
${userHealth.issues.map(issue => `- **${issue.type}**: ${issue.message}`).join('\n')}
` : '✅ **No issues detected** - All sessions within normal parameters'}
${userHealth.flags.length > 0 ? `
#### Session Flags:
${userHealth.flags.map(flag => `- ${flag}`).join('\n')}
` : ''}
## 📋 SESSION DETAILS
${user.sessions.map((session, index) => `
### Session ${index + 1}
- **Start:** ${new Date(session.start).toLocaleString()}
- **End:** ${new Date(session.end).toLocaleString()}
- **Duration:** ${session.duration.hours.toFixed(1)}h
- **End Type:** ${session.duration.endEventType} (${session.duration.endEventPriority})
- **Status:** ${session.validation.isValid ? '✅ Valid' : '❌ Issues'}
${session.validation.flags.length > 0 ? `- **Flags:** ${session.validation.flags.join(', ')}` : ''}
`).join('')}
---
*Report generated by Enhanced Volunteer Time Daemon v2.0*
*Network Theory Applied Research Institute, Inc.*`;
}
// Helper methods for status indicators
getHoursStatus(hours) {
if (hours > 60) return '🔴 Excessive';
if (hours > 40) return '🟡 High';
return '🟢 Normal';
}
getAvgSessionStatus(avg) {
if (avg > 12) return '🟡 Long';
return '🟢 Normal';
}
getLongestSessionStatus(longest) {
if (longest > 20) return '🔴 Extreme';
if (longest > 12) return '🟡 Long';
return '🟢 Normal';
}
/**
* Handle notifications for critical issues
*/
async handleNotifications(validationReport) {
if (validationReport.overallHealth === 'CRITICAL') {
await this.sendAdminAlert(validationReport);
}
// Send individual user notifications for critical issues
for (const issue of validationReport.criticalIssues) {
if (issue.userId) {
await this.sendUserAlert(issue);
}
}
}
/**
* Send admin alert for critical system health
*/
async sendAdminAlert(validationReport) {
try {
const adminEmail = 'admin@ntari.org'; // Configure as needed
const emailContent = `
<h2>🚨 Volunteer Time System Alert</h2>
<p><strong>System Health:</strong> ${validationReport.overallHealth}</p>
<p><strong>Critical Issues:</strong> ${validationReport.criticalIssues.length}</p>
<p><strong>Warnings:</strong> ${validationReport.warnings.length}</p>
<h3>Critical Issues:</h3>
<ul>
${validationReport.criticalIssues.map(issue => `<li>${issue.message}</li>`).join('')}
</ul>
<p>Please review the volunteer time analysis dashboard for detailed information.</p>
`;
await emailContact({
emailTo: adminEmail,
subject: '🚨 CRITICAL: Volunteer Time Analysis Alert',
htmlBody: emailContent
});
} catch (error) {
console.error('Failed to send admin alert:', error);
}
}
/**
* Send user alert for individual critical issues
*/
async sendUserAlert(issue) {
try {
// In a real implementation, you would look up user email from members database
// const userEmail = await this.getUserEmail(issue.userId);
console.log(`Would send user alert to ${issue.user} for ${issue.type}`);
// Example email content structure
const emailContent = `
<h2>Volunteer Time Health Notice</h2>
<p>Dear ${issue.user},</p>
<p>Our volunteer time analysis has identified a pattern that may benefit from attention:</p>
<p><strong>${issue.message}</strong></p>
<p>Please consider reviewing your recent work patterns for sustainability and well-being.</p>
<p>For support or questions, please contact your Program Director.</p>
`;
// Uncomment when user email lookup is implemented
// await emailContact({
// emailTo: userEmail,
// subject: 'Volunteer Time Health Notice',
// htmlBody: emailContent
// });
} catch (error) {
console.error('Failed to send user alert:', error);
}
}
/**
* Log error to database
*/
async logError(error) {
try {
await wixData.save("SystemLogs", {
type: 'ERROR',
source: 'VolunteerTimeDaemon',
message: error.message,
stack: error.stack,
timestamp: new Date()
});
} catch (logError) {
console.error('Failed to log error:', logError);
}
}
}
// ============================================================================
// BACKEND: scheduledJobs.js
// ============================================================================
import { scheduleJob } from 'wix-crm-backend';
import { VolunteerTimeDaemon } from './volunteerTimeAnalysis.js';
/**
* Schedule the volunteer time analysis to run every 6 hours
*/
export function setupVolunteerTimeSchedule() {
// Schedule analysis every 6 hours
scheduleJob(
'volunteer-time-analysis',
'0 */6 * * *', // Cron expression: every 6 hours
runScheduledAnalysis
);
console.log('✅ Volunteer time analysis scheduled every 6 hours');
}
/**
* Scheduled analysis function
*/
async function runScheduledAnalysis() {
console.log('🔄 Running scheduled volunteer time analysis...');
try {
const daemon = new VolunteerTimeDaemon();
const result = await daemon.runAnalysis();
console.log('✅ Scheduled analysis completed successfully');
return result;
} catch (error) {
console.error('❌ Scheduled analysis failed:', error);
throw error;
}
}
// ============================================================================
// FRONTEND: Admin Dashboard (admin-volunteer-reports.js)
// ============================================================================
import wixData from 'wix-data';
import { local } from 'wix-storage';
$w.onReady(function () {
loadLatestAnalysis();
loadUserReports();
});
/**
* Load latest analysis results
*/
async function loadLatestAnalysis() {
try {
const results = await wixData.query("VolunteerAnalysis")
.descending("generatedAt")
.limit(1)
.find();
if (results.items.length > 0) {
const analysis = results.items[0];
displayAnalysisSummary(analysis);
}
} catch (error) {
console.error('Failed to load analysis:', error);
}
}
/**
* Load user reports
*/
async function loadUserReports() {
try {
const results = await wixData.query("UserReports")
.descending("generatedAt")
.limit(50)
.find();
displayUserReports(results.items);
} catch (error) {
console.error('Failed to load user reports:', error);
}
}
/**
* Display analysis summary
*/
function displayAnalysisSummary(analysis) {
$w('#healthStatus').text = analysis.overallHealth;
$w('#totalUsers').text = analysis.totalUsers.toString();
$w('#totalHours').text = analysis.totalHours.toFixed(1) + 'h';
$w('#extremeSessions').text = analysis.extremeSessions.toString();
// Set health status color
const healthColor = analysis.overallHealth === 'CRITICAL' ? '#ff4444' :
analysis.overallHealth === 'WARNING' ? '#ffaa00' : '#44ff44';
$w('#healthStatus').style.color = healthColor;
// Display issues
if (analysis.criticalIssues.length > 0) {
const issuesText = analysis.criticalIssues.map(issue => issue.message).join('\n');
$w('#criticalIssues').text = issuesText;
$w('#criticalIssues').show();
} else {
$w('#criticalIssues').hide();
}
}
/**
* Display user reports in repeater
*/
function displayUserReports(reports) {
$w('#userReportsRepeater').data = reports.map(report => ({
_id: report._id,
userName: report.userName,
healthStatus: report.healthStatus,
totalHours: report.totalHours.toFixed(1) + 'h',
sessionCount: report.sessionCount.toString(),
longestSession: report.longestSession.toFixed(1) + 'h',
reportId: report._id
}));
}
/**
* Handle user report click
*/
export function userReportsRepeater_click(event) {
const reportId = event.context.reportId;
local.setItem('selectedReportId', reportId);
wixLocation.to('/user-report-detail');
}
/**
* Force new analysis
*/
export async function forceAnalysisButton_click(event) {
try {
$w('#forceAnalysisButton').disable();
$w('#forceAnalysisButton').label = 'Running Analysis...';
// This would call the backend analysis function
// In practice, you might trigger this via wix-backend or HTTP function
setTimeout(() => {
$w('#forceAnalysisButton').enable();
$w('#forceAnalysisButton').label = 'Force Analysis';
loadLatestAnalysis();
loadUserReports();
}, 5000);
} catch (error) {
console.error('Failed to force analysis:', error);
$w('#forceAnalysisButton').enable();
$w('#forceAnalysisButton').label = 'Force Analysis';
}
}
// ============================================================================
// DATABASE COLLECTIONS SCHEMA
// ============================================================================
/*
Collection: VolunteerAnalysis
Fields:
- periodStart (Date)
- periodEnd (Date)
- overallHealth (Text)
- totalUsers (Number)
- totalHours (Number)
- extremeSessions (Number)
- longSessions (Number)
- validationErrors (Number)
- criticalIssues (Object)
- warnings (Object)
- summary (Object)
- validation (Object)
- generatedAt (Date)
Collection: UserReports
Fields:
- userId (Text)
- userName (Text)
- periodStart (Date)
- periodEnd (Date)
- healthStatus (Text)
- totalHours (Number)
- sessionCount (Number)
- averageSessionLength (Number)
- longestSession (Number)
- reportContent (Text) - Full markdown report
- criticalIssues (Object)
- warnings (Object)
- flags (Object)
- generatedAt (Date)
Collection: SystemLogs
Fields:
- type (Text) - ERROR, INFO, WARNING
- source (Text) - Component name
- message (Text)
- stack (Text) - Error stack trace
- timestamp (Date)
*/