'use client'; import { useState, useEffect, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Legend, CartesianGrid, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar, } from 'recharts'; import { Users, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { cn } from '@/lib/utils'; interface UserOption { graphUserId: string; displayName: string; email: string; jobTitle: string | null; department: string | null; } interface DayData { date: string; hoursWorked: number; billableHours: number; } interface MonthData { month: string; hoursWorked: number; billableHours: number; daysWorked: number; teamsMessages: number; teamsPrivateMessages: number; teamsCalls: number; meetingsAttended: number; meetingsOrganized: number; emailsSent: number; emailsReceived: number; totalMeetings: number; clientMeetings: number; meetingDurationMinutes: number; zoomCalls: number; zoomClientCalls: number; } interface HistoryData { user: { id: string; displayName: string; email: string; jobTitle: string | null; department: string | null; autotaskResourceId: number | null; }; daily: DayData[]; monthly: MonthData[]; } // Heat level class names — must be full strings for Tailwind to include them const HEAT_CLASSES = [ 'bg-muted/50', 'bg-emerald-100 dark:bg-emerald-950', 'bg-emerald-300 dark:bg-emerald-800', 'bg-emerald-500 dark:bg-emerald-600', 'bg-emerald-700 dark:bg-emerald-400', ]; function hoursLevel(h: number): number { if (h <= 0) return 0; if (h < 2) return 1; if (h < 5) return 2; if (h < 7) return 3; return 4; } function ActivityHeatmap({ daily }: { daily: DayData[] }) { const dailyMap: Record = {}; for (const d of daily) dailyMap[d.date] = d; const today = new Date(); today.setHours(0, 0, 0, 0); // Start from 52 weeks ago, padded back to Monday const startDate = new Date(today); startDate.setDate(startDate.getDate() - 363); const dow = (startDate.getDay() + 6) % 7; // Mon=0 … Sun=6 startDate.setDate(startDate.getDate() - dow); const yearAgo = new Date(today); yearAgo.setFullYear(yearAgo.getFullYear() - 1); // Build weeks const weeks: Array> = []; const cursor = new Date(startDate); while (cursor <= today) { const week: Array<{ date: string; inRange: boolean }> = []; for (let i = 0; i < 7; i++) { const key = cursor.toISOString().slice(0, 10); week.push({ date: key, inRange: cursor >= yearAgo && cursor <= today }); cursor.setDate(cursor.getDate() + 1); } weeks.push(week); } // Month label: track where each month starts const monthLabels: Array<{ weekIndex: number; label: string }> = []; let lastMonth = -1; weeks.forEach((week, wi) => { const d = new Date(week[0].date + 'T00:00:00'); const m = d.getMonth(); if (m !== lastMonth) { monthLabels.push({ weekIndex: wi, label: d.toLocaleDateString('en-US', { month: 'short' }), }); lastMonth = m; } }); const DAY_LABELS = ['Mon', '', 'Wed', '', 'Fri', '', 'Sun']; return (
{/* Day labels */}
{DAY_LABELS.map((label, i) => (
{label}
))}
{/* Grid */}
{/* Month labels */}
{weeks.map((_, wi) => { const ml = monthLabels.find(m => m.weekIndex === wi); return (
{ml && ( {ml.label} )}
); })}
{/* Cells */}
{weeks.map((week, wi) => (
{week.map((cell, di) => { const data = dailyMap[cell.date]; const hours = data?.hoursWorked ?? 0; const level = cell.inRange ? hoursLevel(hours) : 0; const isFuture = cell.date > today.toISOString().slice(0, 10); return (
); })}
))}
{/* Legend */}
Less {HEAT_CLASSES.map((cls, i) => (
))} More
); } function buildRadarData(monthly: MonthData[]) { const active = monthly.filter(m => m.hoursWorked > 0 || m.teamsMessages > 0 || m.emailsSent > 0); if (active.length === 0) return []; const avg = (fn: (m: MonthData) => number) => active.reduce((s, m) => s + fn(m), 0) / active.length; const avgHours = avg(m => m.hoursWorked); const avgBillable = avg(m => m.billableHours); const avgMeetings = avg(m => m.totalMeetings); const avgComms = avg(m => m.teamsMessages + m.emailsSent); const avgCalls = avg(m => m.zoomClientCalls + m.teamsCalls); return [ { subject: 'Utilization', value: Math.min(100, Math.round((avgHours / 160) * 100)), fullMark: 100, }, { subject: 'Billable %', value: avgHours > 0 ? Math.round((avgBillable / avgHours) * 100) : 0, fullMark: 100, }, { subject: 'Meetings', value: Math.min(100, Math.round((avgMeetings / 25) * 100)), fullMark: 100, }, { subject: 'Comms', value: Math.min(100, Math.round((avgComms / 400) * 100)), fullMark: 100, }, { subject: 'Client Calls', value: Math.min(100, Math.round((avgCalls / 15) * 100)), fullMark: 100, }, ]; } function monthLabel(m: string) { return new Date(m + '-02').toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); } interface BackfillStatus { running: boolean; started: string | null; processed: number; total: number; currentUser: string | null; errors: number; done: boolean; log: string[]; } export default function EngagementProfilePage() { const [users, setUsers] = useState([]); const [usersLoading, setUsersLoading] = useState(true); const [selectedUserId, setSelectedUserId] = useState(''); const [history, setHistory] = useState(null); const [historyLoading, setHistoryLoading] = useState(false); const [backfill, setBackfill] = useState(null); const [backfillStarting, setBackfillStarting] = useState(false); useEffect(() => { (async () => { try { const res = await fetch('/api/engagement/users?period=D30&sort=display_name&order=asc'); const data = await res.json(); setUsers(data.users ?? []); } catch {} setUsersLoading(false); })(); }, []); const loadHistory = useCallback(async (userId: string) => { setHistoryLoading(true); setHistory(null); try { const res = await fetch(`/api/engagement/user/${userId}/history`); const data = await res.json(); setHistory(data); } catch {} setHistoryLoading(false); }, []); const handleSelect = (userId: string) => { setSelectedUserId(userId); loadHistory(userId); }; const startBackfill = async () => { setBackfillStarting(true); try { await fetch('/api/engagement/backfill-meetings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ monthsBack: 12 }), }); pollBackfill(); } catch {} setBackfillStarting(false); }; const pollBackfill = useCallback(async () => { const res = await fetch('/api/engagement/backfill-meetings').catch(() => null); if (!res) return; const data: BackfillStatus = await res.json(); setBackfill(data); if (data.running) setTimeout(pollBackfill, 2000); else if (data.done && selectedUserId) loadHistory(selectedUserId); }, [selectedUserId, loadHistory]); useEffect(() => { fetch('/api/engagement/backfill-meetings').then(r => r.json()).then((d: BackfillStatus) => { setBackfill(d); if (d.running) setTimeout(pollBackfill, 2000); }).catch(() => {}); }, [pollBackfill]); const monthly = history?.monthly ?? []; const radarData = buildRadarData(monthly); const totalHours = monthly.reduce((s, m) => s + m.hoursWorked, 0); const totalBillable = monthly.reduce((s, m) => s + m.billableHours, 0); const billablePct = totalHours > 0 ? Math.round((totalBillable / totalHours) * 100) : 0; const activeMonths = monthly.filter(m => m.hoursWorked > 0).length; const peakMonth = monthly.reduce( (best, m) => (m.hoursWorked > (best?.hoursWorked ?? 0) ? m : best), null as MonthData | null ); const barData = monthly.map(m => ({ name: m.month.slice(5), billable: parseFloat(m.billableHours.toFixed(1)), nonBillable: parseFloat((m.hoursWorked - m.billableHours).toFixed(1)), })); return (

Employee Profile

12-month activity overview

{/* Backfill panel */} {backfill && (backfill.running || backfill.done) ? ( 0 ? 'border-yellow-400' : 'border-green-400')}>
{backfill.running && } {backfill.running ? `Backfilling meetings… ${backfill.processed}/${backfill.total} users` : `Backfill complete — ${backfill.processed} users, ${backfill.errors} errors`}
{backfill.running && backfill.currentUser && ( {backfill.currentUser} )}
{backfill.running && backfill.total > 0 && (
)} {backfill.log.length > 0 && (
                {backfill.log.slice(-20).join('\n')}
              
)} ) : (
)} {!selectedUserId && (

Select an employee

Choose an employee above to see their 12-month activity profile

)} {selectedUserId && historyLoading && (
{[...Array(4)].map((_, i) => ( ))}
)} {selectedUserId && !historyLoading && history && ( <> {/* User header */}

{history.user.displayName}

{history.user.jobTitle && {history.user.jobTitle}} {history.user.jobTitle && history.user.department && ·} {history.user.department && {history.user.department}} {(history.user.jobTitle || history.user.department) && ·} {history.user.email}
{/* Year stats */}

Total Hours

{totalHours.toFixed(0)}

over 12 months

Billable Rate

{billablePct}%

{totalBillable.toFixed(0)}h billable

Avg hrs / month

{activeMonths > 0 ? (totalHours / activeMonths).toFixed(0) : '—'}

{activeMonths} active months

Peak Month

{peakMonth ? peakMonth.hoursWorked.toFixed(0) + 'h' : '—'}

{peakMonth ? monthLabel(peakMonth.month) : ''}

{/* Activity heatmap */} Activity Calendar

Daily hours worked — last 12 months

{history.daily.length > 0 ? ( ) : (

No time entry data available

)}
{/* Monthly bar chart + radar */}
Monthly Hours

Billable vs non-billable

[ `${value}h`, name === 'billable' ? 'Billable' : 'Non-billable', ]} contentStyle={{ fontSize: 12 }} /> (v === 'billable' ? 'Billable' : 'Non-billable')} wrapperStyle={{ fontSize: 11 }} />
Activity Signature

12-month average profile

{radarData.length > 0 ? ( ) : (

No data

)}
{/* Monthly breakdown table */} Monthly Breakdown Month Hours Billable Bill % Days Meetings Messages Emails Calls {[...monthly].reverse().map(m => { const pct = m.hoursWorked > 0 ? Math.round((m.billableHours / m.hoursWorked) * 100) : 0; const isEmpty = m.hoursWorked === 0 && m.teamsMessages === 0 && m.emailsSent === 0; const totalCalls = m.zoomClientCalls + m.teamsCalls; return ( {monthLabel(m.month)} {m.hoursWorked > 0 ? m.hoursWorked.toFixed(1) : '—'} {m.billableHours > 0 ? m.billableHours.toFixed(1) : '—'} {m.hoursWorked > 0 ? `${pct}%` : '—'} {m.daysWorked > 0 ? m.daysWorked : '—'} {m.totalMeetings > 0 ? m.totalMeetings : '—'} {m.teamsMessages > 0 ? m.teamsMessages : '—'} {m.emailsSent > 0 ? m.emailsSent : '—'} {totalCalls > 0 ? totalCalls : '—'} ); })}
)}
); }