feat: performance dashboard counts per delivery, submission tab shows deliveries, task row hover fix

- Team performance: count each delivery record individually (sent_by + version_number), not per task — R00/R01/R02 by different people all count separately
- Performance uses deliveries table directly, EST timezone bucketing, month-gated tabs
- TaskDetail Submissions tab: reads from deliveries table, shows R## version, sent_by, sent_at
- Submit for Review: writes to deliveries + delivery_files (deliveries bucket), stores version_number
- Revision request popup: uses FileAttachment drag-drop component
- Task table: only Name and Project columns highlight on hover
- deliveries.version_number column added and backfilled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-02 22:21:58 -04:00
parent 96e90561b9
commit 2c387c074f
2 changed files with 28 additions and 33 deletions
+26 -20
View File
@@ -530,7 +530,7 @@ function ClientHighlightCard({ highlights }) {
);
}
function TeamPerformanceCard({ tasks, profiles }) {
function TeamPerformanceCard({ tasks, profiles, deliveries }) {
const navigate = useNavigate();
const profileMap = useMemo(() => {
const m = new Map();
@@ -538,40 +538,43 @@ function TeamPerformanceCard({ tasks, profiles }) {
return m;
}, [profiles]);
const doneStatuses = ['client_approved', 'invoiced', 'paid'];
const toEST = (dateStr) => new Date(new Date(dateStr).toLocaleString('en-US', { timeZone: 'America/New_York' }));
const monthsWithData = useMemo(() => new Set(
(deliveries || []).filter(d => d.submission?.task).map(d => {
const dt = toEST(d.sent_at);
return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}`;
})
), [deliveries]); // eslint-disable-line react-hooks/exhaustive-deps
const allMonthOpts = useMemo(() => {
const opts = [];
const now = new Date();
const now = toEST(new Date().toISOString());
for (let i = 0; i < 6; i++) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
opts.push({ value: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`, label: i === 0 ? 'This Month' : d.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }) });
}
return opts;
}, []);
const monthsWithData = useMemo(() => new Set(
(tasks || []).filter(t => doneStatuses.includes(t.status) && t.completed_at).map(t => {
const d = new Date(t.completed_at);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
})
), [tasks]); // eslint-disable-line react-hooks/exhaustive-deps
const monthOpts = allMonthOpts.filter(o => monthsWithData.has(o.value));
const [monthKey, setMonthKey] = useState(() => monthOpts[0]?.value || allMonthOpts[0].value);
const { people, totalDone } = useMemo(() => {
const [year, month] = monthKey.split('-').map(Number);
const map = new Map();
(tasks || []).forEach(t => {
if (!doneStatuses.includes(t.status) || !t.completed_at) return;
const d = new Date(t.completed_at);
if (d.getFullYear() !== year || d.getMonth() + 1 !== month) return;
if (!t.assigned_name) return;
const entry = map.get(t.assigned_name) || { name: t.assigned_name, id: t.assigned_to || null, newCount: 0, revCount: 0 };
if ((t.current_version || 0) === 0) entry.newCount += 1;
(deliveries || []).forEach(d => {
const task = d.submission?.task;
if (!task) return;
const dt = toEST(d.sent_at);
if (dt.getFullYear() !== year || dt.getMonth() + 1 !== month) return;
const name = d.sent_by || task.assigned_name;
if (!name) return;
const entry = map.get(name) || { name, id: task.assigned_to || null, newCount: 0, revCount: 0 };
if ((d.version_number || 0) === 0) entry.newCount += 1;
else entry.revCount += 1;
map.set(t.assigned_name, entry);
map.set(name, entry);
});
const people = [...map.values()].map(p => ({ ...p, done: p.newCount + p.revCount })).sort((a, b) => b.done - a.done);
return { people, totalDone: people.reduce((s, p) => s + p.done, 0) };
}, [tasks, monthKey]); // eslint-disable-line react-hooks/exhaustive-deps
}, [deliveries, monthKey]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<div style={{ background: 'var(--card-bg)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', border: '1px solid var(--border)', borderRadius: 8, padding: '18px 21px', display: 'flex', flexDirection: 'column' }}>
@@ -584,7 +587,7 @@ function TeamPerformanceCard({ tasks, profiles }) {
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" style={{ position: 'absolute', right: 0, pointerEvents: 'none' }} stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round"><polyline points="1,2 4,6 7,2"/></svg>
</div>
</div>
{people.slice(0, 5).length === 0 ? (
{people.length === 0 ? (
<div className="card-empty-center">No completed tasks this month</div>
) : (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
@@ -643,6 +646,7 @@ export default function TeamDashboard() {
const [teamInvoices, setTeamInvoices] = useState(() => cached?.teamInvoices || []);
const [teamExpenses, setTeamExpenses] = useState([]);
const [myInvoices, setMyInvoices] = useState([]);
const [perfDeliveries, setPerfDeliveries] = useState([]);
const [loading, setLoading] = useState(!cached);
useEffect(() => {
@@ -731,7 +735,7 @@ export default function TeamDashboard() {
? supabase.from('expenses').select('amount')
: Promise.resolve({ data: [] });
const [{ data: t }, { data: p }, { data: subs }, { data: profiles }, { data: activity }, { data: cos }, { data: memRows }, { data: invs }, { data: exps }] = await withTimeout(Promise.all([
const [{ data: t }, { data: p }, { data: subs }, { data: profiles }, { data: activity }, { data: cos }, { data: memRows }, { data: invs }, { data: exps }, { data: delivs }] = await withTimeout(Promise.all([
tasksQuery,
projectsQuery,
submissionsQuery,
@@ -741,6 +745,7 @@ export default function TeamDashboard() {
supabase.from('company_members').select('company_id, profile_id'),
invoicesPromise,
expensesPromise,
supabase.from('deliveries').select('sent_by, sent_at, version_number, submission:submissions!inner(task:tasks!inner(assigned_to, assigned_name))').gte('sent_at', cutoffStr),
]), 30000, 'Dashboard load');
if (cancelled) return;
@@ -768,6 +773,7 @@ export default function TeamDashboard() {
setTeamInvoices(isTeam ? (invs || []) : []);
setMyInvoices(!isTeam ? (invs || []) : []);
setTeamExpenses(exps || []);
setPerfDeliveries(delivs || []);
if (isTeam) {
writePageCache(CACHE_KEY, {
@@ -871,7 +877,7 @@ export default function TeamDashboard() {
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, marginTop: 24 }}>
<HotItemsCard submissions={submissions} tasks={tasks} isClient={isClient} />
<TeamPerformanceCard tasks={tasks} profiles={allProfiles} />
<TeamPerformanceCard tasks={tasks} profiles={allProfiles} deliveries={perfDeliveries} />
</div>
{isTeam && (
<div style={{ marginTop: 24 }}>