// Team screen — members by department + workload. (() => { const { Card, Badge, Avatar, ProgressBar } = window.TechyFuelOSDesignSystem_be0222; const DEPT_TONE = { Design: 'violet', Video: 'teal', Marketing: 'success', Development: 'info', Sales: 'warning', Admin: 'neutral', Content: 'teal', Leadership: 'brand' }; const ROLE_LABEL = { owner: 'Owner', admin: 'Admin', member: 'Member' }; function Team() { useLucide(); const [team, setTeam] = React.useState([]); const [loading, setLoading] = React.useState(true); const [taskCounts, setTaskCounts] = React.useState({}); const [progress, setProgress] = React.useState({}); // memberId -> { total, done, pct, secondsThisMonth, secondsAllTime } const [modalOpen, setModalOpen] = React.useState(false); const [saving, setSaving] = React.useState(false); const [form, setForm] = React.useState({ name: '', email: '', department: '', role: 'member' }); function set(k, v) { setForm(f => ({ ...f, [k]: v })); } React.useEffect(() => { if (!window.API) { setLoading(false); return; } window.API.getAllTeamMembers().then(r => { setTeam(r.data || []); }).catch(() => {}).finally(() => setLoading(false)); window.API.getTasks().then(r => { if (!r.data) return; const counts = {}; r.data.filter(t => t.status !== 'done').forEach(t => { if (t.assigned_to) counts[t.assigned_to] = (counts[t.assigned_to] || 0) + 1; }); setTaskCounts(counts); const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); (window.API.getAllTimeEntries ? window.API.getAllTimeEntries() : Promise.resolve({ data: [] })).then(er => { const entries = er.data || []; const byMember = {}; r.data.forEach(t => { if (!t.assigned_to) return; if (!byMember[t.assigned_to]) byMember[t.assigned_to] = { total: 0, done: 0, secondsThisMonth: 0, secondsAllTime: 0 }; byMember[t.assigned_to].total++; if (t.status === 'done') byMember[t.assigned_to].done++; }); entries.forEach(e => { if (!e.member_id) return; if (!byMember[e.member_id]) byMember[e.member_id] = { total: 0, done: 0, secondsThisMonth: 0, secondsAllTime: 0 }; byMember[e.member_id].secondsAllTime += e.duration_seconds || 0; if (new Date(e.started_at) >= monthStart) byMember[e.member_id].secondsThisMonth += e.duration_seconds || 0; }); const result = {}; Object.keys(byMember).forEach(id => { const b = byMember[id]; result[id] = { ...b, pct: b.total > 0 ? Math.round((b.done / b.total) * 100) : 0 }; }); setProgress(result); }).catch(() => {}); }).catch(() => {}); }, []); function fmtHours(totalSeconds) { if (!totalSeconds) return '0h'; const h = totalSeconds / 3600; return h < 1 ? Math.round(totalSeconds / 60) + 'm' : h.toFixed(1) + 'h'; } async function handleInviteMember() { if (!form.name.trim()) { alert('Name is required.'); return; } if (!form.email.trim()) { alert('Email is required.'); return; } setSaving(true); try { const payload = { name: form.name.trim(), email: form.email.trim(), role: form.role, status: 'active', }; if (form.department) payload.department = form.department; if (window.API) { const { data, error } = await window.API.addTeamMember(payload); if (error) { const msg = error.message || error.details || JSON.stringify(error); alert('Could not add member:\n\n' + msg); return; } if (data) setTeam(prev => [...prev, data]); } setModalOpen(false); setForm({ name: '', email: '', department: '', role: 'member' }); } catch (err) { alert('Error: ' + (err.message || JSON.stringify(err))); } finally { setSaving(false); } } async function toggleStatus(m, e) { e.stopPropagation(); const suspending = m.status !== 'inactive'; if (suspending) { const warn = m.role === 'owner' ? '\n\nThis member is an Owner — suspending them will block their access too.' : ''; if (!window.confirm(`Suspend ${m.name}? They will lose access to TechyFuel OS immediately.${warn}`)) return; } const newStatus = suspending ? 'inactive' : 'active'; setTeam(prev => prev.map(t => t.id === m.id ? { ...t, status: newStatus } : t)); if (window.API) { try { await window.API.setTeamMemberStatus(m.id, newStatus); } catch {} } } async function removeMember(m, e) { e.stopPropagation(); const warn = m.role === 'owner' ? '\n\nWarning: this member is an Owner.' : ''; if (!window.confirm(`Remove ${m.name} permanently? This deletes their team profile and cannot be undone.${warn}`)) return; const prev = team; setTeam(p => p.filter(t => t.id !== m.id)); if (window.API && window.API.deleteTeamMember) { try { const { error } = await window.API.deleteTeamMember(m.id); if (error) { setTeam(prev); alert('Could not remove member:\n\n' + (error.message || error.details || JSON.stringify(error))); } } catch (err) { setTeam(prev); alert('Error removing member: ' + (err.message || JSON.stringify(err))); } } } const departments = [...new Set(team.map(m => m.department).filter(Boolean))]; const maxTasks = Math.max(...team.map(m => taskCounts[m.id] || 0), 1); return (

Team

{team.length} members · {departments.length} departments

{departments.map(d => { const tone = DEPT_TONE[d] || 'neutral'; const dotColor = tone === 'neutral' ? 'var(--slate-400)' : tone === 'info' ? 'var(--blue-500)' : `var(--${tone}-500)`; return ( {d} ); })}
{loading &&
Loading…
} {!loading && team.length === 0 && (
No team members yet
Invite your first teammate to get started.
)} {!loading && team.length > 0 && (
{team.map((m, i) => { const tasks = taskCounts[m.id] || 0; const load = maxTasks > 0 ? Math.round((tasks / maxTasks) * 100) : 0; const tone = DEPT_TONE[m.department] || 'neutral'; const suspended = m.status === 'inactive'; const prog = progress[m.id] || { total: 0, done: 0, pct: 0, secondsThisMonth: 0, secondsAllTime: 0 }; return (
{m.name} {suspended && Suspended}
{ROLE_LABEL[m.role] || m.role}
{m.department || 'Team'}
Workload 85 ? 'var(--red-600)' : 'var(--text-strong)', fontWeight: 'var(--fw-bold)' }}>{load}%
85 ? 'danger' : load > 70 ? 'warning' : 'brand'} size="sm" />
Progress · {prog.done}/{prog.total} tasks done {prog.pct}%
{fmtHours(prog.secondsThisMonth)} this month {fmtHours(prog.secondsAllTime)} all time
{tasks} active tasks {m.email || '—'}
); })}
)} setModalOpen(false)} title="Invite member" onSubmit={handleInviteMember} loading={saving} submitLabel="Add member">
set('name', e.target.value)} /> set('email', e.target.value)} />
set('department', e.target.value)} />
); } Object.assign(window, { Team }); })();