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:
@@ -17,7 +17,6 @@ import { fmtShortDate } from '../lib/dates';
|
||||
import { popupOverlayStyle } from '../lib/popupStyles';
|
||||
import { useLiveRefresh } from '../hooks/useLiveRefresh';
|
||||
import { getTaskDerivedState } from '../lib/taskVersions';
|
||||
import { buildInvoiceStatusByKey, deriveVersionStatus } from '../lib/invoiceVersionRules';
|
||||
import {
|
||||
TASK_TABLE_TH_STYLE,
|
||||
TASK_TABLE_TD_BASE,
|
||||
@@ -50,7 +49,6 @@ export default function ProjectDetailPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submissions, setSubmissions] = useState([]);
|
||||
const [deliveries, setDeliveries] = useState([]);
|
||||
const [invoiceItems, setInvoiceItems] = useState([]);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
|
||||
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);
|
||||
if (actErr) console.error('activity_log fetch:', actErr);
|
||||
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) {
|
||||
const { data: ext } = await supabase.from('profiles').select('id, name, avatar_url, email').eq('role', 'external').order('name');
|
||||
setExtProfs(ext || []);
|
||||
@@ -239,21 +229,15 @@ export default function ProjectDetailPage() {
|
||||
if (loading) return <Layout><PageLoader /></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.flatMap(task => {
|
||||
const rows = tasks.map(task => {
|
||||
const derived = getTaskDerivedState(task, submissions, deliveries);
|
||||
const versionSet = new Set(derived.taskSubs.map(s => Number(s.version_number ?? 0)));
|
||||
versionSet.add(derived.currentVersion);
|
||||
return [...versionSet].sort((a, b) => a - b).map(v => ({
|
||||
id: task.id,
|
||||
rowKey: `${task.id}:${v}`,
|
||||
title: task.title,
|
||||
status: deriveVersionStatus(task, v, invoiceStatusByKey),
|
||||
version: v,
|
||||
return {
|
||||
id: task.id, rowKey: task.id, title: task.title, status: task.status,
|
||||
version: derived.currentVersion,
|
||||
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,
|
||||
submittedAt: derived.latestActivityAt ? new Date(derived.latestActivityAt).toISOString() : (task.submitted_at || ''),
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
const sortedRows = sort(rows, (r, key) => {
|
||||
@@ -319,7 +303,7 @@ export default function ProjectDetailPage() {
|
||||
</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;
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 30, flexWrap: 'wrap' }}>
|
||||
|
||||
+10
-32
@@ -21,7 +21,6 @@ import { popupOverlayStyle } from '../lib/popupStyles';
|
||||
import { useLiveRefresh } from '../hooks/useLiveRefresh';
|
||||
import { resolveScopedWorkIds } from '../lib/workScope';
|
||||
import { mergeSubmissionDisplayNames } from '../lib/submissionDisplay';
|
||||
import { buildInvoiceStatusByKey, deriveVersionStatus } from '../lib/invoiceVersionRules';
|
||||
import {
|
||||
TASK_TABLE_TH_STYLE,
|
||||
TASK_TABLE_TD_BASE,
|
||||
@@ -207,7 +206,6 @@ export default function RequestsPage() {
|
||||
const [tasks, setTasks] = useState(() => teamCached?.tasks || extCached?.tasks || []);
|
||||
const [submissions, setSubmissions] = useState(() => teamCached?.submissions || extCached?.submissions || []);
|
||||
const [deliveries, setDeliveries] = useState([]);
|
||||
const [invoiceItems, setInvoiceItems] = useState([]);
|
||||
const [companies, setCompanies] = useState(() => teamCached?.companies || []);
|
||||
const [loading, setLoading] = useState(() => {
|
||||
if (isTeam) return !teamCached;
|
||||
@@ -323,16 +321,6 @@ export default function RequestsPage() {
|
||||
setDeliveries(deliveryRows);
|
||||
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) {
|
||||
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 || ''));
|
||||
}, [isClient, companies, currentUser]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const invoiceStatusByKey = useMemo(() => buildInvoiceStatusByKey(invoiceItems), [invoiceItems]);
|
||||
|
||||
// Normalize tasks → one row per version for independent tracking/billing
|
||||
// Normalize all tasks to a common row shape for unified table render
|
||||
const allRows = useMemo(() => {
|
||||
if (isClient) {
|
||||
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);
|
||||
if (derived.visibleTaskSubs.length === 0 || !derived.deadlineSource) return [];
|
||||
// One row per version that has a submission, always include current version
|
||||
const versionSet = new Set(derived.taskSubs.map(s => Number(s.version_number ?? 0)));
|
||||
versionSet.add(derived.currentVersion);
|
||||
return [...versionSet].sort((a, b) => a - b).map(v => ({
|
||||
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,
|
||||
if (derived.visibleTaskSubs.length === 0 || !derived.deadlineSource) return null;
|
||||
return {
|
||||
id: task.id, rowKey: task.id, title: task.title, status: task.status, projectId: task.project_id,
|
||||
serviceType: derived.serviceType, deadline: derived.deadline,
|
||||
version: derived.currentVersion,
|
||||
isHot: derived.isHot,
|
||||
assignedName: task.assigned_name || null, assigneeAvatar: task.assignee?.avatar_url || null, assignedTo: task.assigned_to || null,
|
||||
submittedAt: derived.latestActivityAt,
|
||||
}));
|
||||
});
|
||||
}, [tasks, submissions, deliveries, invoiceStatusByKey, isClient]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}, [tasks, submissions, deliveries, isClient]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const filteredRows = useMemo(() => {
|
||||
return allRows
|
||||
|
||||
Reference in New Issue
Block a user