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:
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user