// Executive Dashboard screen. (() => { const { StatCard, Card, Badge, Avatar, AvatarGroup, ProgressBar } = window.TechyFuelOSDesignSystem_be0222; // Time-of-day greeting (Pakistan local time on the device). Was hardcoded to // "Good morning" which looked wrong at night. function timeGreeting() { const h = new Date().getHours(); if (h < 5) return 'Good evening'; if (h < 12) return 'Good morning'; if (h < 17) return 'Good afternoon'; return 'Good evening'; } function SectionHead({ title, action }) { return (

{title}

{action}
); } function LinkBtn({ children, onClick }) { return ; } // Home/reporting currency is PKR — getDashboardStats() already converts // every invoice into PKR (via live FX rates) before summing, regardless of // what currency the client actually paid in. function fmtMoney(n) { if (!n) return '₨0'; if (n >= 1000000) return '₨' + (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return '₨' + (n / 1000).toFixed(1) + 'K'; return '₨' + Math.round(n); } function fxToPKR(amount, currency, rates) { const n = Number(amount); if (!n) return 0; if (!currency || currency === 'PKR') return n; if (!rates) return 0; const usd = currency === 'USD' ? n : (rates[currency] ? n / rates[currency] : null); if (usd === null) return 0; return rates.PKR ? usd * rates.PKR : 0; } const PERIODS = [ { id: 'month', label: 'This month' }, { id: 'last_month', label: 'Last month' }, { id: 'quarter', label: 'This quarter' }, { id: 'year', label: 'This year' }, { id: 'all', label: 'All time' }, ]; function fmtDueDate(ds) { if (!ds) return '—'; const d = new Date(ds); const today = new Date(); today.setHours(0, 0, 0, 0); const diff = Math.round((d - today) / 86400000); if (diff === 0) return 'Today'; if (diff === 1) return 'Tomorrow'; return d.toLocaleDateString('en', { month: 'short', day: 'numeric' }); } function fmtWhen(ds) { if (!ds) return ''; const diff = Math.round((Date.now() - new Date(ds)) / 60000); if (diff < 60) return diff + 'm ago'; if (diff < 1440) return Math.round(diff / 60) + 'h ago'; return Math.round(diff / 1440) + 'd ago'; } const STATUS_TONE = { todo: 'brand', in_progress: 'brand', review: 'warning', done: 'success', backlog: 'neutral', }; const STATUS_ICON = { todo: 'circle', in_progress: 'loader', review: 'eye', done: 'check', backlog: 'inbox', }; const STATUS_BG = { todo: ['var(--blue-50)', 'var(--blue-600)'], in_progress: ['var(--violet-50)','var(--violet-600)'], review: ['var(--amber-50)', 'var(--amber-600)'], done: ['var(--green-50)', 'var(--green-600)'], backlog: ['var(--slate-100)','var(--slate-500)'], }; function ActivityRow({ item }) { const [bg, fg] = STATUS_BG[item.status] || STATUS_BG.todo; const icon = STATUS_ICON[item.status] || 'activity'; const label = item.status === 'in_progress' ? 'In progress' : item.status === 'done' ? 'Completed' : item.status === 'review' ? 'In review' : item.status || 'Task'; return (
{item.title} {item.project && · {item.project}} {item.assignee && — {item.assignee}}
{fmtWhen(item.created_at)}
); } // ── Member dashboard — only their own tasks/projects/chat, no company-wide // figures (revenue, client list, other people's work) ────────────────── function MemberDashboard() { useLucide(); const [greeting, setGreeting] = React.useState((window.TFMyName || '').split(' ')[0] || ''); const [myTasks, setMyTasks] = React.useState([]); const [unread, setUnread] = React.useState(0); const [loading, setLoading] = React.useState(true); const [att, setAtt] = React.useState(null); // today's attendance record const [attLinked, setAttLinked] = React.useState(true); // is the user in the employee directory? const [attBusy, setAttBusy] = React.useState(false); React.useEffect(() => { if (!window.API) { setLoading(false); return; } const myId = window.TFMyMemberId; (async () => { try { const r = await window.API.getTasks({ assignedTo: myId }); if (r.data) setMyTasks(r.data); } catch {} try { const count = await window.API.getUnreadCount(myId); setUnread(count || 0); } catch {} try { if (window.API.getMyAttendance) { const a = await window.API.getMyAttendance(); setAtt(a.data || null); setAttLinked(a.linked !== false); } } catch {} setLoading(false); })(); }, []); async function doCheckIn() { if (!window.API?.selfCheckIn) return; setAttBusy(true); try { const r = await window.API.selfCheckIn(); if (r.data) setAtt(r.data); else if (r.message) alert(r.message); } catch { alert('Could not check in. Please try again.'); } setAttBusy(false); } async function doCheckOut() { if (!window.API?.selfCheckOut) return; setAttBusy(true); try { const r = await window.API.selfCheckOut(); if (r.data) setAtt(r.data); else if (r.message) alert(r.message); } catch { alert('Could not check out. Please try again.'); } setAttBusy(false); } const fmtT = (t) => { if (!t) return '—'; try { return new Date(String(t).replace(' ', 'T') + (String(t).includes('Z') ? '' : 'Z')).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' }); } catch { return '—'; } }; const openTasks = myTasks.filter(t => t.status !== 'done'); const doneTasks = myTasks.filter(t => t.status === 'done'); const upcoming = openTasks.filter(t => t.due_date).sort((a, b) => new Date(a.due_date) - new Date(b.due_date)); const overdue = upcoming.filter(t => new Date(t.due_date) < new Date()); const myProjects = Array.from(new Map(myTasks.filter(t => t.project_id && t.projects).map(t => [t.project_id, t.projects])).values()); if (loading) return
Loading…
; return (
{greeting ? `${timeGreeting()}, ${greeting}` : timeGreeting()}

My dashboard

{/* My attendance card — today's actions + stats + history (break sessions each on their own line). Shares state with the top-bar dropdown via the same API, so no more out-of-sync "double". */} {window.MyAttendance &&
}
} tone="brand" /> } tone={overdue.length ? 'danger' : 'neutral'} /> } tone="success" />
window.TFNavigate && window.TFNavigate('tasks')}>View all tasks} /> {upcoming.length === 0 ? (
No upcoming tasks — you're all caught up!
) : (
{upcoming.slice(0, 8).map(t => { const isOverdue = new Date(t.due_date) < new Date(); return (
{t.title}
{t.projects?.name &&
{t.projects.name}
}
{fmtDueDate(t.due_date)}
); })}
)}
{myProjects.length === 0 ? (
No projects yet
) : (
{myProjects.map((p, i) => (
window.TFNavigate && window.TFNavigate('projects')} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 0', cursor: 'pointer' }}> {p.name}
))}
)}
{unread > 0 ? `${unread} unread message${unread === 1 ? '' : 's'}` : 'All caught up'}
window.TFNavigate && window.TFNavigate('chat')}>Open chat
); } const TODAY_KEY = new Date().toISOString().slice(0, 10); function DailyBriefing({ briefing, onDismiss, onOpenAI }) { if (!briefing) return null; const { overdueTasks, dueTodayTasks, overdueInvoices, overdueInvoiceTotal, weekDeadlines } = briefing; const nothingUrgent = overdueTasks === 0 && dueTodayTasks === 0 && overdueInvoices === 0; const lines = []; if (overdueTasks > 0) lines.push({ icon: 'alert-circle', color: 'var(--red-600)', text: `${overdueTasks} task${overdueTasks === 1 ? '' : 's'} overdue` }); if (dueTodayTasks > 0) lines.push({ icon: 'clock', color: 'var(--amber-600)', text: `${dueTodayTasks} task${dueTodayTasks === 1 ? '' : 's'} due today` }); if (overdueInvoices > 0) lines.push({ icon: 'wallet', color: 'var(--red-600)', text: `${overdueInvoices} invoice${overdueInvoices === 1 ? '' : 's'} overdue (${fmtMoney(overdueInvoiceTotal)})` }); if (weekDeadlines > 0) lines.push({ icon: 'calendar-days', color: 'var(--blue-600)', text: `${weekDeadlines} deadline${weekDeadlines === 1 ? '' : 's'} this week` }); return (
Today's briefing
{nothingUrgent ? (
Nothing urgent — everything's on track. 🎉
) : (
{lines.map((l, i) => ( {l.text} ))}
)} {!nothingUrgent && ( )}
); } function ExecutiveDashboard() { useLucide(); // Revenue/invoices are owner+admin only. A manager gets the same operational // dashboard minus the money figures. const canSeeRevenue = ['owner', 'admin'].includes(window.TFMyRole); const [stats, setStats] = React.useState({ activeClients: 0, activeProjects: 0, openTasks: 0, revenue: 0 }); const [deadlines, setDeadlines] = React.useState([]); const [activity, setActivity] = React.useState([]); const [tasksByStatus, setTasksByStatus] = React.useState({ todo: 0, in_progress: 0, review: 0, done: 0 }); const [greeting, setGreeting] = React.useState((window.TFMyName || '').split(' ')[0] || ''); const [clients, setClients] = React.useState([]); const [modalOpen, setModalOpen] = React.useState(false); const [saving, setSaving] = React.useState(false); const [statsLoaded, setStatsLoaded] = React.useState(false); const [form, setForm] = React.useState({ name: '', client_id: '', budget: '', due_date: '', priority: 'medium', status: 'active' }); const [period, setPeriod] = React.useState('month'); const [periodOpen, setPeriodOpen] = React.useState(false); const periodRef = React.useRef(null); const [briefing, setBriefing] = React.useState(null); const [briefingDismissed, setBriefingDismissed] = React.useState(() => localStorage.getItem('tf_briefing_dismissed') === TODAY_KEY); const [announcements, setAnnouncements] = React.useState([]); function set(k, v) { setForm(f => ({ ...f, [k]: v })); } React.useEffect(() => { function onClickOutside(e) { if (periodRef.current && !periodRef.current.contains(e.target)) setPeriodOpen(false); } document.addEventListener('mousedown', onClickOutside); return () => document.removeEventListener('mousedown', onClickOutside); }, []); // Revenue is the only figure that's period-scoped — refetch it whenever // the period picker changes. React.useEffect(() => { if (!window.API) return; (async () => { try { const s = await window.API.getDashboardStats(period); if (s) { setStats(s); setStatsLoaded(true); } } catch {} })(); }, [period]); React.useEffect(() => { if (!window.API || !window.API.getAnnouncements) return; (async () => { try { const { data } = await window.API.getAnnouncements(); if (Array.isArray(data)) setAnnouncements(data.slice(0, 3)); } catch {} })(); }, []); React.useEffect(() => { if (!window.API) return; (async () => { try { const r = await window.API.getTasks(); if (r.data) { const upcoming = r.data .filter(t => t.due_date && t.status !== 'done') .sort((a, b) => new Date(a.due_date) - new Date(b.due_date)) .slice(0, 4) .map(t => ({ project: t.title, client: t.clients ? t.clients.name : (t.projects ? t.projects.name : ''), due: fmtDueDate(t.due_date), urgent: new Date(t.due_date) < new Date(), pct: 50, team: t.team_members ? [t.team_members.name] : [], })); setDeadlines(upcoming); const counts = { todo: 0, in_progress: 0, review: 0, done: 0 }; r.data.forEach(t => { if (counts[t.status] !== undefined) counts[t.status]++; }); setTasksByStatus(counts); const recent = [...r.data] .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) .slice(0, 5) .map(t => ({ id: t.id, title: t.title, status: t.status, project: t.projects?.name || '', assignee: t.team_members?.name || '', created_at: t.created_at })); setActivity(recent); const today0 = new Date(); today0.setHours(0, 0, 0, 0); const weekAhead = new Date(today0.getTime() + 7 * 86400000); const openTasksWithDue = r.data.filter(t => t.status !== 'done' && t.due_date); const overdueTasks = openTasksWithDue.filter(t => new Date(t.due_date) < today0).length; const dueTodayTasks = openTasksWithDue.filter(t => { const d = new Date(t.due_date); d.setHours(0, 0, 0, 0); return d.getTime() === today0.getTime(); }).length; const weekDeadlines = openTasksWithDue.filter(t => { const d = new Date(t.due_date); return d >= today0 && d <= weekAhead; }).length; let overdueInvoices = 0, overdueInvoiceTotal = 0; try { // Only owners/admins can read invoices — fetch the finance widget // ONLY for them. Anyone else (incl. before the role has resolved) // skips the call so members never fire an invoices 403. if (window.TFMyRole !== 'owner' && window.TFMyRole !== 'admin') throw new Error('skip-finance'); const invRes = await window.API.getInvoices(); const fx = window.API.getFxRates ? await window.API.getFxRates().catch(() => null) : null; const rates = fx && fx.rates; (invRes.data || []).filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled' && inv.due_date && new Date(inv.due_date) < today0).forEach(inv => { overdueInvoices++; overdueInvoiceTotal += fxToPKR(inv.amount, inv.currency, rates); }); } catch {} setBriefing({ overdueTasks, dueTodayTasks, weekDeadlines, overdueInvoices, overdueInvoiceTotal }); } } catch {} // Greeting name = the LOGGED-IN user, not the first row of the team list // (that bug showed e.g. "Subhan" in Abubakar's account). if (window.TFMyName) setGreeting(String(window.TFMyName).split(' ')[0]); try { const r = await window.API.getClients(); if (r.data) setClients(r.data); } catch {} })(); }, []); function dismissBriefing() { localStorage.setItem('tf_briefing_dismissed', TODAY_KEY); setBriefingDismissed(true); } async function handleNewProject() { if (!form.name.trim()) return; setSaving(true); try { const payload = { name: form.name, status: form.status, priority: form.priority }; if (form.client_id) payload.client_id = form.client_id; if (form.budget) payload.budget = Number(form.budget); if (form.due_date) payload.due_date = form.due_date; if (window.API) await window.API.createProject(payload); setModalOpen(false); setForm({ name: '', client_id: '', budget: '', due_date: '', priority: 'medium', status: 'active' }); if (window.TFNavigate) window.TFNavigate('projects'); } finally { setSaving(false); } } // Privacy toggle: hide revenue figures on screen (shoulder-surfing). Per-device. const [hideRevenue, setHideRevenue] = React.useState(() => { try { return localStorage.getItem('tf_hide_revenue') === '1'; } catch (e) { return false; } }); const toggleHideRevenue = () => setHideRevenue(v => { const nv = !v; try { localStorage.setItem('tf_hide_revenue', nv ? '1' : '0'); } catch (e) {} return nv; }); const RevEye = () => ( ); const MASK = '••••••'; const revenueDisplay = hideRevenue ? MASK : fmtMoney(stats.revenue); const totalTasks = Object.values(tasksByStatus).reduce((s, n) => s + n, 0); const donutSegments = [ { value: tasksByStatus.in_progress || 0, color: 'var(--blue-600)' }, { value: tasksByStatus.review || 0, color: 'var(--sky-500)' }, { value: tasksByStatus.todo || 0, color: 'var(--violet-500)' }, { value: tasksByStatus.done || 0, color: 'var(--green-500)' }, ]; return (
{greeting ? `${timeGreeting()}, ${greeting}` : timeGreeting()}

Executive dashboard

{periodOpen && (
{PERIODS.map(p => (
{ setPeriod(p.id); setPeriodOpen(false); }} style={{ padding: '9px 14px', fontSize: 'var(--text-sm)', fontWeight: p.id === period ? 'var(--fw-bold)' : 'var(--fw-medium)', color: p.id === period ? 'var(--blue-600)' : 'var(--text-body)', background: p.id === period ? 'var(--blue-50)' : 'transparent', cursor: 'pointer' }} onMouseEnter={e => { if (p.id !== period) e.currentTarget.style.background = 'var(--slate-50)'; }} onMouseLeave={e => { if (p.id !== period) e.currentTarget.style.background = 'transparent'; }}> {p.label}
))}
)}
{!briefingDismissed && window.TFOpenAI && window.TFOpenAI()} />} {announcements.length > 0 && ( window.TFNavigate && window.TFNavigate('announcements')}>View all} />
{announcements.map(a => (
window.TFNavigate && window.TFNavigate('announcements')} style={{ display: 'flex', alignItems: 'flex-start', gap: 10, padding: '8px 0', borderTop: '1px solid var(--border-subtle)', cursor: 'pointer' }}>
{a.title}
{a.body &&
{a.body}
}
{a.author_name || 'Admin'}
{a.image_url && }
))}
)}
{canSeeRevenue && } tone="success" />} } tone="brand" /> } tone="warning" /> } tone="violet" />
{canSeeRevenue && (
{revenueDisplay} collected · {(PERIODS.find(p => p.id === period) || PERIODS[0]).label.toLowerCase()}
{hideRevenue ? (
Revenue hidden · tap the eye to show
) : statsLoaded && stats.revenue === 0 ? (
No paid invoices yet
) : ( )}
)} {totalTasks === 0 ? (
No tasks yet
) : (
{[ ['In progress', tasksByStatus.in_progress, 'var(--blue-600)'], ['Review', tasksByStatus.review, 'var(--sky-500)'], ['Todo', tasksByStatus.todo, 'var(--violet-500)'], ['Done', tasksByStatus.done, 'var(--green-500)'], ].map(([l, n, c]) => (
{l} {n}
))}
)}
{activity.length === 0 ? (
No tasks yet
) : (
{activity.map((a, i) => )}
)}
window.TFNavigate && window.TFNavigate('calendar')}>Open calendar} /> {deadlines.length === 0 ? (
No upcoming deadlines
) : (
{deadlines.map((d, i) => (
{d.project}
{d.client}
{d.due}
))}
)}
setModalOpen(false)} title="New project" onSubmit={handleNewProject} loading={saving} submitLabel="Create project"> set('name', e.target.value)} />
set('budget', e.target.value)} /> set('due_date', e.target.value)} />
); } // Members get their own personalized view (no company-wide revenue/client // figures); everyone else gets the full executive dashboard. function Dashboard() { return window.TFMyRole === 'member' ? : ; } Object.assign(window, { Dashboard, TFSectionHead: SectionHead, TFLinkBtn: LinkBtn }); })();