import { DIRECTOR_DASHBOARD_COMPLETION_TREND_THRESHOLD, DIRECTOR_DASHBOARD_FRAME_PREVIEW_LIMIT, DIRECTOR_DASHBOARD_HIGH_INCOMPLETE_STAFF_THRESHOLD, DIRECTOR_DASHBOARD_TEXT_PREVIEW_LENGTH, } from '@/shared/constants/directorDashboard'; import { countStaffAttendanceStatus, staffAttendanceRate, } from '@/business/staff-attendance/selectors'; import type { FrameEntryViewModel } from '@/business/frame/types'; import type { StaffAttendanceRecordViewModel } from '@/business/staff-attendance/types'; import type { PolicyAcknowledgmentReportDto, PolicyAcknowledgmentReportSummaryDto, } from '@/shared/types/policyDocuments'; import type { DirectorAcknowledgmentDocumentRow, DirectorFramePreview, DirectorOverviewCard, DirectorQuizResultDetail, DirectorQuizResultRow, DirectorRiskArea, } from '@/business/director-dashboard/types'; import type { SafetyQuizCompletionSummary, SafetyQuizComplianceRow, } from '@/business/safety-quiz/types'; import type { PersonalityCompletionDto, PersonalityQuizResultDto, } from '@/shared/types/personality'; import type { ZoneCheckinCompletionDto } from '@/shared/types/zoneCheckins'; export function calculateQuizCompletionRate( quizSummary: SafetyQuizCompletionSummary, ): number { return quizSummary.completionRate; } export function buildDirectorOverviewCards( attendanceRecords: readonly StaffAttendanceRecordViewModel[], quizSummary: SafetyQuizCompletionSummary, frameEntries: readonly FrameEntryViewModel[], acknowledgmentSummary?: PolicyAcknowledgmentReportSummaryDto | null, ): readonly DirectorOverviewCard[] { const attendanceRate = staffAttendanceRate(attendanceRecords); const quizCompletionRate = calculateQuizCompletionRate(quizSummary); const acknowledgmentRate = acknowledgmentSummary?.completionRate ?? 0; return [ { label: 'Staff Attendance', value: `${attendanceRate}%`, change: `${attendanceRecords.length} records`, trend: 'up', iconId: 'clock', tone: 'orange', module: 'attendance', }, { label: 'Behavior Management Quiz Completion', value: `${quizSummary.completedCount}/${quizSummary.totalStaff}`, change: `${quizCompletionRate}%`, trend: quizCompletionRate > DIRECTOR_DASHBOARD_COMPLETION_TREND_THRESHOLD ? 'up' : 'down', iconId: 'shield', tone: 'blue', module: 'qbs', action: 'openQuizResults', }, { label: 'F.R.A.M.E. Entries', value: frameEntries.length.toString(), change: 'Total entries', trend: 'up', iconId: 'eye', tone: 'amber', module: 'frame', }, { label: 'Staff Members', value: quizSummary.totalStaff.toString(), change: 'Active', trend: 'up', iconId: 'users', tone: 'purple', module: 'user-admin', }, { label: 'Acknowledgments', value: `${acknowledgmentRate}%`, change: `${acknowledgmentSummary?.missingCount ?? 0} missing`, trend: acknowledgmentRate > DIRECTOR_DASHBOARD_COMPLETION_TREND_THRESHOLD ? 'up' : 'down', iconId: 'clipboard', tone: 'emerald', module: 'director', action: 'openAcknowledgments', }, ]; } export function buildDirectorRiskAreas( attendanceRecords: readonly StaffAttendanceRecordViewModel[], quizSummary: SafetyQuizCompletionSummary, emotionalIntelligenceCompletion?: PersonalityCompletionDto | null, zoneCheckinCompletion?: ZoneCheckinCompletionDto | null, acknowledgmentDocuments: readonly DirectorAcknowledgmentDocumentRow[] = [], ): readonly DirectorRiskArea[] { const incompleteStaffCount = quizSummary.pendingCount; const absenceCount = countStaffAttendanceStatus(attendanceRecords, 'absent'); const lateCount = countStaffAttendanceStatus(attendanceRecords, 'late'); const staffAttendanceExceptionCount = absenceCount + lateCount; const selfAssessmentPendingCount = emotionalIntelligenceCompletion?.summary ? emotionalIntelligenceCompletion.summary.totalStaff - emotionalIntelligenceCompletion.summary.selfAssessmentCompletedCount : 0; const personalityPendingCount = emotionalIntelligenceCompletion?.summary ? emotionalIntelligenceCompletion.summary.totalStaff - emotionalIntelligenceCompletion.summary.personalityCompletedCount : 0; const zonePendingCount = zoneCheckinCompletion?.summary.pendingCount ?? 0; const nonGreenZoneNote = getNonGreenZoneNote(zoneCheckinCompletion); const risks: DirectorRiskArea[] = []; if (incompleteStaffCount > 0) { risks.push({ issue: `${incompleteStaffCount} staff haven't completed Behavior Management quiz`, severity: incompleteStaffCount > DIRECTOR_DASHBOARD_HIGH_INCOMPLETE_STAFF_THRESHOLD ? 'high' : 'medium', module: 'qbs', action: 'openQuizResults', }); } if (selfAssessmentPendingCount > 0) { risks.push({ issue: `${selfAssessmentPendingCount} staff haven't completed EI self-assessment`, severity: 'low', module: 'ei', action: 'openQuizResults', }); } if (personalityPendingCount > 0) { risks.push({ issue: `${personalityPendingCount} staff haven't completed personality type quiz`, severity: 'low', module: 'ei', action: 'openQuizResults', }); } if (zonePendingCount > 0) { risks.push({ issue: `${zonePendingCount} staff haven't completed daily zone check-in`, severity: 'medium', module: 'zones', action: 'openQuizResults', }); } if (nonGreenZoneNote) { risks.push({ issue: nonGreenZoneNote, severity: 'medium', module: 'zones', }); } const acknowledgmentRisk = buildAcknowledgmentRisk(acknowledgmentDocuments); if (acknowledgmentRisk) { risks.push({ issue: acknowledgmentRisk.issue, severity: acknowledgmentRisk.severity, module: 'director', action: 'openAcknowledgments', }); } if (staffAttendanceExceptionCount > 0) { risks.push({ issue: `${staffAttendanceExceptionCount} ${pluralize('staff attendance exception', staffAttendanceExceptionCount)} this period (${lateCount} late, ${absenceCount} absent)`, severity: 'high', module: 'attendance', }); } return risks; } function buildAcknowledgmentRisk( acknowledgmentDocuments: readonly DirectorAcknowledgmentDocumentRow[], ): Pick | null { const unresolvedDocuments = acknowledgmentDocuments.filter((document) => document.missingCount > 0, ); if (unresolvedDocuments.length === 0) { return null; } const staffIds = new Set(); for (const document of unresolvedDocuments) { for (const staff of document.missingStaff) { staffIds.add(staff.userId); } } const staffCount = staffIds.size || Math.max( ...unresolvedDocuments.map((document) => document.missingCount), ); const documentCount = unresolvedDocuments.length; return { issue: `${staffCount} ${pluralize('staff', staffCount)} haven't acknowledged ${documentCount} ${pluralize('document', documentCount)}`, severity: staffCount > DIRECTOR_DASHBOARD_HIGH_INCOMPLETE_STAFF_THRESHOLD || documentCount > DIRECTOR_DASHBOARD_HIGH_INCOMPLETE_STAFF_THRESHOLD ? 'high' : 'medium', }; } export function buildDirectorAcknowledgmentDocuments( report?: PolicyAcknowledgmentReportDto | null, ): readonly DirectorAcknowledgmentDocumentRow[] { if (!report) { return []; } return report.documents.map((document) => { const missingStaff = report.staff .filter((staff) => staff.documents.some((status) => status.policyDocumentId === document.id && status.version === document.version && status.missing, ), ) .map((staff) => ({ userId: staff.userId, name: staff.name, avatar: staff.avatar, tenantName: staff.tenantName, tenantLogo: staff.tenantLogo, role: formatRoleLabel(staff.role), email: staff.email, })); return { id: document.id, title: document.title, category: document.category, categoryLabel: getPolicyCategoryLabel(document.category), version: document.version, totalStaff: document.totalStaff, acknowledgedCount: document.acknowledgedCount, missingCount: document.missingCount, completionRate: document.completionRate, missingStaff, }; }); } function getNonGreenZoneNote( zoneCheckinCompletion?: ZoneCheckinCompletionDto | null, ): string | null { const nonGreenRows = zoneCheckinCompletion?.rows .filter((row) => row.status === 'complete' && row.zone !== null && row.zone !== 'green') ?? []; if (nonGreenRows.length === 0) { return null; } const staffNotes = nonGreenRows .map((row) => `${row.name} (${row.result})`) .join(', '); return `Non-green regulation zones: ${staffNotes}`; } function getPolicyCategoryLabel(category: string): string { if (category === 'safety_protocol') { return 'Safety Protocols'; } if (category === 'handbook_policy') { return 'Handbook & Policies'; } return category.replace(/_/g, ' '); } function formatRoleLabel(role: string | null): string { return role ? role.replace(/_/g, ' ') : 'Staff'; } function pluralize(noun: string, count: number): string { if (noun === 'staff') { return 'staff'; } return count === 1 ? noun : `${noun}s`; } export function buildDirectorQuizResults( safetyRows: readonly SafetyQuizComplianceRow[], emotionalIntelligenceCompletion?: PersonalityCompletionDto | null, zoneCheckinCompletion?: ZoneCheckinCompletionDto | null, tenantLabel = 'Current scope', ): readonly DirectorQuizResultRow[] { const rowsByUserId = new Map(); const ensureRow = ( userId: string, staffName: string, avatar: string | null, tenantName: string | null, tenantLogo: string | null, role: string | null, ): { staffName: string; avatar: string | null; tenantName: string | null; tenantLogo: string | null; role: string; details: DirectorQuizResultDetail[]; } => { const existing = rowsByUserId.get(userId); if (existing) { if (!existing.avatar && avatar) { existing.avatar = avatar; } if (!existing.tenantName && tenantName) { existing.tenantName = tenantName; } if (!existing.tenantLogo && tenantLogo) { existing.tenantLogo = tenantLogo; } return existing; } const next = { staffName, avatar, tenantName, tenantLogo, role: role ?? 'Staff', details: [], }; rowsByUserId.set(userId, next); return next; }; for (const row of safetyRows) { ensureRow(row.userId, row.name, row.avatar, row.tenantName, row.tenantLogo, row.role).details.push({ id: 'behavior-management', quiz: 'Behavior Management', result: row.score, date: row.date, status: row.status, }); } for (const row of emotionalIntelligenceCompletion?.rows ?? []) { const target = ensureRow(row.userId, row.name, row.avatar, row.tenantName, row.tenantLogo, row.role); target.details.push( { id: 'ei-self-assessment', quiz: 'EI Self-Assessment', result: formatPersonalityQuizResult(row.selfAssessment, 'Pending'), date: formatPersonalityQuizDate(row.selfAssessment), status: row.selfAssessment ? 'complete' : 'pending', }, { id: 'personality-type', quiz: 'Personality Type Quiz', result: formatPersonalityQuizResult(row.personality, 'Pending'), date: formatPersonalityQuizDate(row.personality), status: row.personality ? 'complete' : 'pending', }, ); } for (const row of zoneCheckinCompletion?.rows ?? []) { ensureRow(row.userId, row.name, row.avatar, row.tenantName, row.tenantLogo, row.role).details.push({ id: 'daily-zone-check-in', quiz: 'Daily Zone Check-In', result: row.result, date: row.status === 'complete' ? new Date(`${row.date}T00:00:00`).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : 'Not completed', status: row.status, }); } return [...rowsByUserId.entries()].map(([userId, row]) => ({ id: userId, staffName: row.staffName, avatar: row.avatar, tenant: row.tenantName ?? tenantLabel, tenantLogo: row.tenantLogo, role: row.role, completedCount: row.details.filter((detail) => detail.status === 'complete').length, totalCount: row.details.length, details: row.details, })); } export function buildDirectorFramePreviews( frameEntries: readonly FrameEntryViewModel[], ): readonly DirectorFramePreview[] { return frameEntries.slice(0, DIRECTOR_DASHBOARD_FRAME_PREVIEW_LIMIT).map((entry) => ({ id: entry.id, week: entry.weekOf, sections: [ { letter: 'F', text: truncatePreview(entry.formal) }, { letter: 'R', text: truncatePreview(entry.recognition) }, { letter: 'A', text: truncatePreview(entry.application) }, { letter: 'M', text: truncatePreview(entry.management) }, { letter: 'E', text: truncatePreview(entry.emotional) }, ], })); } function truncatePreview(value: string): string { if (value.length <= DIRECTOR_DASHBOARD_TEXT_PREVIEW_LENGTH) { return value; } return `${value.slice(0, DIRECTOR_DASHBOARD_TEXT_PREVIEW_LENGTH)}...`; } function formatPersonalityQuizResult( result: PersonalityQuizResultDto | null, fallback: string, ): string { if (!result) { return fallback; } if (result.personality_type) { return result.personality_type; } if (result.result_label && result.score !== null) { return `${result.result_label} (${result.score}/${result.total_questions * 4})`; } return result.result_label ?? 'Completed'; } function formatPersonalityQuizDate(result: PersonalityQuizResultDto | null): string { if (!result) { return 'Not completed'; } return new Date(result.completed_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', }); }