feat: per-version R## tracking and billing separation

Each revision (R00, R01, R02) is now an independent trackable unit:

invoiceVersionRules: add deriveVersionStatus, buildInvoiceStatusByKey,
  parseVersionFromItemDescription for per-version status derivation

Tasks.jsx + ProjectDetail.jsx:
  - Fetch invoice_items joined with invoice.status (team only)
  - allRows/rows now flatMap per version (one row per R##)
  - Status derived from: invoice_items > past version > task.status
  - R00 paid shows as paid; R01 in_progress shows separately
  - tr keys use rowKey (taskId:version) to avoid duplicate key warnings

Invoice lifecycle (TeamInvoiceDetail, TeamInvoices, TeamCreateInvoice):
  - Remove task.status sync on invoice paid/sent/created
  - Keep invoiced:true flag for double-billing prevention
  - Task work status (not_started → client_approved) stays work-flow only

ProjectDetail completion bar uses task.status === client_approved only
  (no longer polluted by invoiced/paid status from invoice sync)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-09 22:36:36 -05:00
parent da0e5d5c39
commit 5b2d5b65a4
6 changed files with 97 additions and 28 deletions
+35
View File
@@ -16,3 +16,38 @@ export function getRevisionChargeQuantity(versionNumber) {
const version = Number(versionNumber || 0);
return version >= 2 ? 1 : 0;
}
// Parses version number from an invoice item description like "Project • Task R01"
export function parseVersionFromItemDescription(description = '') {
const match = String(description).match(/[\-]\s*R(\d{2})\b/i);
return match ? Number(match[1]) : 0;
}
// Builds a Map<"taskId:version", "sent"|"paid"> from invoice_items rows
// (each row must have invoice.status joined)
export function buildInvoiceStatusByKey(invoiceItems = []) {
const map = new Map();
for (const item of invoiceItems) {
const status = item.invoice?.status;
if (!status || !['sent', 'paid'].includes(status)) continue;
const version = parseVersionFromItemDescription(item.description);
const key = `${item.task_id}:${version}`;
const existing = map.get(key);
// paid beats sent
if (!existing || (status === 'paid' && existing !== 'paid')) {
map.set(key, status);
}
}
return map;
}
// Per-version status: invoice state takes priority over task work state
export function deriveVersionStatus(task, version, invoiceStatusByKey) {
const invoiceStatus = invoiceStatusByKey?.get(`${task.id}:${version}`);
if (invoiceStatus === 'paid') return 'paid';
if (invoiceStatus === 'sent') return 'invoiced';
// Past version — work moved on, implicitly approved
if (version < (task.current_version ?? 0)) return 'client_approved';
// Current version — use task work status
return task.status;
}