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