Skip to content

Commit 1e2f6ee

Browse files
leopu00claude
andcommitted
feat(team): pagina /team — tabella worker con stato online, ruolo, ultimo task e filtri
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 16aa9ea commit 1e2f6ee

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

web/app/team/page.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
'use client'
2+
3+
import { useState, useEffect } from 'react'
4+
import { FilterBar, FilterDef, FilterValues } from '../components/FilterBar'
5+
import { EmptyState } from '../components/EmptyState'
6+
7+
type Member = {
8+
id: string; name: string; role: string; session: string
9+
online: boolean; last_task: { id: string; stato: string } | null
10+
}
11+
12+
const TASK_COLORS: Record<string, string> = {
13+
completed: 'var(--color-green)', merged: 'var(--color-green)',
14+
'in-progress': 'var(--color-blue)', 'pr-ready': 'var(--color-yellow)',
15+
blocked: 'var(--color-red)',
16+
}
17+
18+
function TaskBadge({ task }: { task: Member['last_task'] }) {
19+
if (!task) return <span style={{ color: 'var(--color-dim)', fontSize: 10 }}></span>
20+
const color = TASK_COLORS[task.stato] ?? 'var(--color-dim)'
21+
return (
22+
<span className="inline-flex gap-1.5 items-center px-2 py-0.5 rounded text-[9px] font-mono"
23+
style={{ border: `1px solid ${color}44`, background: `${color}11`, color }}>
24+
{task.id} · {task.stato}
25+
</span>
26+
)
27+
}
28+
29+
export default function TeamPage() {
30+
const [members, setMembers] = useState<Member[]>([])
31+
const [loading, setLoading] = useState(true)
32+
const [filters, setFilters] = useState<FilterValues>({ role: '', status: '' })
33+
34+
useEffect(() => {
35+
const load = () => fetch('/api/team').then(r => r.json()).then(({ team }) => { setMembers(team ?? []); setLoading(false) })
36+
.catch(() => setLoading(false))
37+
load()
38+
const id = setInterval(load, 15_000)
39+
return () => clearInterval(id)
40+
}, [])
41+
42+
const roles = [...new Set(members.map(m => m.role))]
43+
const defs: FilterDef[] = [
44+
{ type: 'select', key: 'role', label: 'Ruolo', value: filters.role,
45+
options: roles.map(r => ({ value: r, label: r })) },
46+
{ type: 'select', key: 'status', label: 'Stato', value: filters.status,
47+
options: [{ value: 'online', label: 'Online' }, { value: 'offline', label: 'Offline' }] },
48+
]
49+
50+
const visible = members.filter(m => {
51+
if (filters.role && m.role !== filters.role) return false
52+
if (filters.status === 'online' && !m.online) return false
53+
if (filters.status === 'offline' && m.online) return false
54+
return true
55+
})
56+
57+
const online = members.filter(m => m.online).length
58+
59+
return (
60+
<main className="min-h-screen px-6 py-10">
61+
<div className="max-w-4xl flex flex-col gap-6">
62+
63+
<div className="flex items-end justify-between flex-wrap gap-4">
64+
<div>
65+
<p className="text-[9px] font-semibold tracking-[0.2em] uppercase mb-1" style={{ color: 'var(--color-green)' }}>sistema</p>
66+
<h1 className="text-xl font-bold" style={{ color: 'var(--color-white)' }}>
67+
Team
68+
{!loading && <span className="ml-3 text-[11px] font-mono" style={{ color: 'var(--color-dim)' }}>
69+
{online}/{members.length} online
70+
</span>}
71+
</h1>
72+
</div>
73+
<FilterBar filters={defs} values={filters}
74+
onChange={(k, v) => setFilters(p => ({ ...p, [k]: v }))}
75+
onClear={() => setFilters({ role: '', status: '' })} />
76+
</div>
77+
78+
{loading ? (
79+
<p className="text-[11px]" style={{ color: 'var(--color-muted)' }}>Caricamento…</p>
80+
) : visible.length === 0 ? (
81+
<EmptyState icon="👥" title="Nessun membro trovato" size="sm" />
82+
) : (
83+
<div className="rounded-xl overflow-hidden" style={{ border: '1px solid var(--color-border)' }}>
84+
<table className="w-full border-collapse">
85+
<thead>
86+
<tr style={{ background: 'var(--color-deep)', borderBottom: '1px solid var(--color-border)' }}>
87+
{['Nome', 'Ruolo', 'Sessione', 'Stato', 'Ultimo task'].map(h => (
88+
<th key={h} className="px-4 py-2.5 text-left text-[9px] font-bold uppercase tracking-wider"
89+
style={{ color: 'var(--color-dim)' }}>{h}</th>
90+
))}
91+
</tr>
92+
</thead>
93+
<tbody>
94+
{visible.map((m, i) => (
95+
<tr key={m.id} style={{
96+
background: 'var(--color-panel)',
97+
borderBottom: i < visible.length - 1 ? '1px solid var(--color-border)' : 'none',
98+
}}>
99+
<td className="px-4 py-3 text-[12px] font-bold" style={{ color: 'var(--color-white)' }}>{m.name}</td>
100+
<td className="px-4 py-3 text-[11px]" style={{ color: 'var(--color-muted)' }}>{m.role}</td>
101+
<td className="px-4 py-3 text-[10px] font-mono" style={{ color: 'var(--color-dim)' }}>{m.session}</td>
102+
<td className="px-4 py-3">
103+
<span className="inline-flex items-center gap-1.5 text-[10px] font-semibold"
104+
style={{ color: m.online ? 'var(--color-green)' : 'var(--color-dim)' }}>
105+
<span style={{ width: 6, height: 6, borderRadius: '50%', background: 'currentColor', display: 'inline-block',
106+
animation: m.online ? 'pulse-dot 2.5s ease-in-out infinite' : 'none' }} />
107+
{m.online ? 'online' : 'offline'}
108+
</span>
109+
</td>
110+
<td className="px-4 py-3"><TaskBadge task={m.last_task} /></td>
111+
</tr>
112+
))}
113+
</tbody>
114+
</table>
115+
</div>
116+
)}
117+
118+
{!loading && <p className="text-[9px]" style={{ color: 'var(--color-dim)' }}>Aggiornamento automatico ogni 15s</p>}
119+
</div>
120+
</main>
121+
)
122+
}

0 commit comments

Comments
 (0)