fix: revert per-version rows; reset stuck revision tasks in DB

Tasks/projects page: reverted back to one row per task (work status only,
no billing state on that page). Per-version R## tracking lives in invoicing
only (invoice_items, sub invoice form).

DB fix: 7 Public Storage tasks had pending revision versions (R01/R02)
with no delivery but were stuck as 'paid' from old invoice lifecycle sync.
Reset to 'not_started' so they appear in the Revisions tab for work:
  68664, 68666, 68721 (R02), 68808, 68809 (R02), 68810 (R02), 68811 (R02)

Removed unused invoiceItems state/fetch from Tasks + ProjectDetail.
invoiceVersionRules helpers retained for invoicing use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-09 22:47:21 -05:00
parent 5b2d5b65a4
commit 1cbf06a6c0
2 changed files with 16 additions and 54 deletions
+6 -22
View File
@@ -17,7 +17,6 @@ import { fmtShortDate } from '../lib/dates';
import { popupOverlayStyle } from '../lib/popupStyles'; import { popupOverlayStyle } from '../lib/popupStyles';
import { useLiveRefresh } from '../hooks/useLiveRefresh'; import { useLiveRefresh } from '../hooks/useLiveRefresh';
import { getTaskDerivedState } from '../lib/taskVersions'; import { getTaskDerivedState } from '../lib/taskVersions';
import { buildInvoiceStatusByKey, deriveVersionStatus } from '../lib/invoiceVersionRules';
import { import {
TASK_TABLE_TH_STYLE, TASK_TABLE_TH_STYLE,
TASK_TABLE_TD_BASE, TASK_TABLE_TD_BASE,
@@ -50,7 +49,6 @@ export default function ProjectDetailPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [submissions, setSubmissions] = useState([]); const [submissions, setSubmissions] = useState([]);
const [deliveries, setDeliveries] = useState([]); const [deliveries, setDeliveries] = useState([]);
const [invoiceItems, setInvoiceItems] = useState([]);
const [activeTab, setActiveTab] = useState('all'); const [activeTab, setActiveTab] = useState('all');
const [editingName, setEditingName] = useState(false); const [editingName, setEditingName] = useState(false);
@@ -113,14 +111,6 @@ export default function ProjectDetailPage() {
const { data: act, error: actErr } = await supabase.from('activity_log').select('id, created_at, actor_name, action, task_title').eq('project_id', id).order('created_at', { ascending: false }).limit(50); const { data: act, error: actErr } = await supabase.from('activity_log').select('id, created_at, actor_name, action, task_title').eq('project_id', id).order('created_at', { ascending: false }).limit(50);
if (actErr) console.error('activity_log fetch:', actErr); if (actErr) console.error('activity_log fetch:', actErr);
setActivityLog((act || []).filter(e => ['task_started', 'task_on_hold', 'task_approved'].includes(e.action))); setActivityLog((act || []).filter(e => ['task_started', 'task_on_hold', 'task_approved'].includes(e.action)));
if (isTeam && taskIds.length > 0) {
const { data: invItems } = await supabase
.from('invoice_items')
.select('task_id, description, invoice:invoices(status)')
.in('task_id', taskIds)
.not('task_id', 'is', null);
setInvoiceItems(invItems || []);
}
if (isTeam) { if (isTeam) {
const { data: ext } = await supabase.from('profiles').select('id, name, avatar_url, email').eq('role', 'external').order('name'); const { data: ext } = await supabase.from('profiles').select('id, name, avatar_url, email').eq('role', 'external').order('name');
setExtProfs(ext || []); setExtProfs(ext || []);
@@ -239,21 +229,15 @@ export default function ProjectDetailPage() {
if (loading) return <Layout><PageLoader /></Layout>; if (loading) return <Layout><PageLoader /></Layout>;
if (!project) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Project not found.</p></Layout>; if (!project) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Project not found.</p></Layout>;
const invoiceStatusByKey = buildInvoiceStatusByKey(invoiceItems); const rows = tasks.map(task => {
const rows = tasks.flatMap(task => {
const derived = getTaskDerivedState(task, submissions, deliveries); const derived = getTaskDerivedState(task, submissions, deliveries);
const versionSet = new Set(derived.taskSubs.map(s => Number(s.version_number ?? 0))); return {
versionSet.add(derived.currentVersion); id: task.id, rowKey: task.id, title: task.title, status: task.status,
return [...versionSet].sort((a, b) => a - b).map(v => ({ version: derived.currentVersion,
id: task.id,
rowKey: `${task.id}:${v}`,
title: task.title,
status: deriveVersionStatus(task, v, invoiceStatusByKey),
version: v,
assignedName: task.assigned_name || null, assigneeAvatar: task.assignee?.avatar_url || null, assignedTo: task.assigned_to || null, assignedName: task.assigned_name || null, assigneeAvatar: task.assignee?.avatar_url || null, assignedTo: task.assigned_to || null,
serviceType: derived.serviceType, deadline: derived.deadline, isHot: derived.isHot, serviceType: derived.serviceType, deadline: derived.deadline, isHot: derived.isHot,
submittedAt: derived.latestActivityAt ? new Date(derived.latestActivityAt).toISOString() : (task.submitted_at || ''), submittedAt: derived.latestActivityAt ? new Date(derived.latestActivityAt).toISOString() : (task.submitted_at || ''),
})); };
}); });
const sortedRows = sort(rows, (r, key) => { const sortedRows = sort(rows, (r, key) => {
@@ -319,7 +303,7 @@ export default function ProjectDetailPage() {
</div> </div>
{(() => { {(() => {
const approved = tasks.filter(t => t.status === 'client_approved').length; const approved = tasks.filter(t => ['client_approved','invoiced','paid'].includes(t.status)).length;
const pct = tasks.length > 0 ? Math.round((approved / tasks.length) * 100) : 0; const pct = tasks.length > 0 ? Math.round((approved / tasks.length) * 100) : 0;
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: 30, flexWrap: 'wrap' }}> <div style={{ display: 'flex', alignItems: 'center', gap: 30, flexWrap: 'wrap' }}>
+10 -32
View File
@@ -21,7 +21,6 @@ import { popupOverlayStyle } from '../lib/popupStyles';
import { useLiveRefresh } from '../hooks/useLiveRefresh'; import { useLiveRefresh } from '../hooks/useLiveRefresh';
import { resolveScopedWorkIds } from '../lib/workScope'; import { resolveScopedWorkIds } from '../lib/workScope';
import { mergeSubmissionDisplayNames } from '../lib/submissionDisplay'; import { mergeSubmissionDisplayNames } from '../lib/submissionDisplay';
import { buildInvoiceStatusByKey, deriveVersionStatus } from '../lib/invoiceVersionRules';
import { import {
TASK_TABLE_TH_STYLE, TASK_TABLE_TH_STYLE,
TASK_TABLE_TD_BASE, TASK_TABLE_TD_BASE,
@@ -207,7 +206,6 @@ export default function RequestsPage() {
const [tasks, setTasks] = useState(() => teamCached?.tasks || extCached?.tasks || []); const [tasks, setTasks] = useState(() => teamCached?.tasks || extCached?.tasks || []);
const [submissions, setSubmissions] = useState(() => teamCached?.submissions || extCached?.submissions || []); const [submissions, setSubmissions] = useState(() => teamCached?.submissions || extCached?.submissions || []);
const [deliveries, setDeliveries] = useState([]); const [deliveries, setDeliveries] = useState([]);
const [invoiceItems, setInvoiceItems] = useState([]);
const [companies, setCompanies] = useState(() => teamCached?.companies || []); const [companies, setCompanies] = useState(() => teamCached?.companies || []);
const [loading, setLoading] = useState(() => { const [loading, setLoading] = useState(() => {
if (isTeam) return !teamCached; if (isTeam) return !teamCached;
@@ -323,16 +321,6 @@ export default function RequestsPage() {
setDeliveries(deliveryRows); setDeliveries(deliveryRows);
setCompanies(co || []); setCompanies(co || []);
// Fetch invoice items for per-version status derivation (team only)
if (isTeam && (t || []).length > 0) {
const taskIdsList = (t || []).map(tk => tk.id);
const { data: invItems } = await supabase
.from('invoice_items')
.select('task_id, description, invoice:invoices(status)')
.in('task_id', taskIdsList)
.not('task_id', 'is', null);
setInvoiceItems(invItems || []);
}
if (isTeam) { if (isTeam) {
writePageCache('team_requests', { submissions: hydratedSubs, tasks: t || [], projects: p || [], companies: co || [] }); writePageCache('team_requests', { submissions: hydratedSubs, tasks: t || [], projects: p || [], companies: co || [] });
@@ -542,9 +530,7 @@ export default function RequestsPage() {
return companies.slice().sort((a, b) => (a.name || '').localeCompare(b.name || '')); return companies.slice().sort((a, b) => (a.name || '').localeCompare(b.name || ''));
}, [isClient, companies, currentUser]); // eslint-disable-line react-hooks/exhaustive-deps }, [isClient, companies, currentUser]); // eslint-disable-line react-hooks/exhaustive-deps
const invoiceStatusByKey = useMemo(() => buildInvoiceStatusByKey(invoiceItems), [invoiceItems]); // Normalize all tasks to a common row shape for unified table render
// Normalize tasks → one row per version for independent tracking/billing
const allRows = useMemo(() => { const allRows = useMemo(() => {
if (isClient) { if (isClient) {
return tasks.map(task => { return tasks.map(task => {
@@ -558,27 +544,19 @@ export default function RequestsPage() {
}; };
}); });
} }
return tasks.flatMap(task => { return tasks.map(task => {
const derived = getTaskDerivedState(task, submissions, deliveries); const derived = getTaskDerivedState(task, submissions, deliveries);
if (derived.visibleTaskSubs.length === 0 || !derived.deadlineSource) return []; if (derived.visibleTaskSubs.length === 0 || !derived.deadlineSource) return null;
// One row per version that has a submission, always include current version return {
const versionSet = new Set(derived.taskSubs.map(s => Number(s.version_number ?? 0))); id: task.id, rowKey: task.id, title: task.title, status: task.status, projectId: task.project_id,
versionSet.add(derived.currentVersion); serviceType: derived.serviceType, deadline: derived.deadline,
return [...versionSet].sort((a, b) => a - b).map(v => ({ version: derived.currentVersion,
id: task.id,
rowKey: `${task.id}:${v}`,
title: task.title,
status: deriveVersionStatus(task, v, invoiceStatusByKey),
projectId: task.project_id,
serviceType: derived.serviceType,
deadline: derived.deadline,
version: v,
isHot: derived.isHot, isHot: derived.isHot,
assignedName: task.assigned_name || null, assigneeAvatar: task.assignee?.avatar_url || null, assignedTo: task.assigned_to || null, assignedName: task.assigned_name || null, assigneeAvatar: task.assignee?.avatar_url || null, assignedTo: task.assigned_to || null,
submittedAt: derived.latestActivityAt, submittedAt: derived.latestActivityAt,
})); };
}); }).filter(Boolean);
}, [tasks, submissions, deliveries, invoiceStatusByKey, isClient]); // eslint-disable-line react-hooks/exhaustive-deps }, [tasks, submissions, deliveries, isClient]); // eslint-disable-line react-hooks/exhaustive-deps
const filteredRows = useMemo(() => { const filteredRows = useMemo(() => {
return allRows return allRows