top of page

NTARI Time Module Management

Public·3 members

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)

*/

7 Views

About

Management of NTARI.org/time Schedule **Every Monday Update...

bottom of page