From e4367e028a5228ef50c4bbd522d0777bc7340b52 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:24:25 +0200 Subject: [PATCH] fix(invoices): badge held (unsent, no send date) invoices as "Draft" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier change only relabeled is_monthly_draft rows. But a per-event invoice created from hours is status 'scheduled' with scheduled_send_at = NULL and is_monthly_draft = false — it never auto-ships (the scheduler only picks rows with scheduled_send_at <= now), yet it still read "Scheduled" on the customer panel + lists. Add a shared isDraftInvoice() helper (scheduled && no send date, or a monthly/manual accumulator) and use it for the badge in the Bills list, the invoice detail header, and the customer profile's invoice panel. A scheduled invoice WITH a future send date keeps "Scheduled". --- .../components/admin/CustomerCrmPanels.tsx | 24 ++++++++++++------- .../src/pages/admin/bills/BillDetailPage.tsx | 10 ++++---- .../src/pages/admin/bills/BillsListPage.tsx | 10 ++++---- frontend/src/services/bills.service.ts | 14 +++++++++++ 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/admin/CustomerCrmPanels.tsx b/frontend/src/components/admin/CustomerCrmPanels.tsx index bc5c3109..6be7b409 100644 --- a/frontend/src/components/admin/CustomerCrmPanels.tsx +++ b/frontend/src/components/admin/CustomerCrmPanels.tsx @@ -19,7 +19,7 @@ import { FileText, Plus, Receipt, ScrollText } from 'lucide-react'; import { Card, Button, Loading } from '../common'; import { useFeatureFlags } from '../../contexts/FeatureFlagsContext'; import { quotesService } from '../../services/quotes.service'; -import { billsService } from '../../services/bills.service'; +import { billsService, isDraftInvoice } from '../../services/bills.service'; import { contractsService } from '../../services/contracts.service'; import { formatMoney } from './LineItemsTable'; import { useLocalizedDate } from '../../hooks/useLocalizedDate'; @@ -204,14 +204,20 @@ const InvoicesPanel: React.FC = ({ customerAccountId }) => { {formatMoney(Number(inv.totalAmountMinor) / 100, inv.currency)} - {t(`bills.status.${inv.status}`, inv.status)} + {isDraftInvoice(inv) ? ( + + {t('bills.status.draft', 'Draft')} + + ) : ( + {t(`bills.status.${inv.status}`, inv.status)} + )} ))} diff --git a/frontend/src/pages/admin/bills/BillDetailPage.tsx b/frontend/src/pages/admin/bills/BillDetailPage.tsx index 24305457..d937c816 100644 --- a/frontend/src/pages/admin/bills/BillDetailPage.tsx +++ b/frontend/src/pages/admin/bills/BillDetailPage.tsx @@ -10,7 +10,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { ArrowLeft, Eye, Send, CheckCircle, BellRing, XCircle, Truck, Edit2, RefreshCw } from 'lucide-react'; import { Button, Card, Loading, Input, LocalizedDateInput } from '../../../components/common'; import { DocumentLineageCard } from '../../../components/admin/DocumentLineageCard'; -import { billsService } from '../../../services/bills.service'; +import { billsService, isDraftInvoice } from '../../../services/bills.service'; import { formatMoney } from '../../../components/admin/LineItemsTable'; import { useLocalizedDate } from '../../../hooks/useLocalizedDate'; import { toast } from 'react-toastify'; @@ -264,10 +264,10 @@ export const BillDetailPage: React.FC = () => { )} - {/* A running monthly/manual accumulator carries status - 'scheduled' but never auto-sends on manual cadence — - read it as "Draft", matching the Bills list. */} - {inv.isMonthlyDraft ? t('bills.status.draft', 'Draft') : t(`bills.status.${inv.status}`, inv.status)} + {/* Held invoice ('scheduled' with no send date, incl. the + monthly/manual accumulator) never auto-ships — read it as + "Draft", matching the Bills list. */} + {isDraftInvoice(inv) ? t('bills.status.draft', 'Draft') : t(`bills.status.${inv.status}`, inv.status)}

diff --git a/frontend/src/pages/admin/bills/BillsListPage.tsx b/frontend/src/pages/admin/bills/BillsListPage.tsx index 793d1b12..192886c8 100644 --- a/frontend/src/pages/admin/bills/BillsListPage.tsx +++ b/frontend/src/pages/admin/bills/BillsListPage.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { Link, useNavigate } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Plus, Search, Upload, X } from 'lucide-react'; -import { billsService, type InvoiceStatus, type InvoiceSort } from '../../../services/bills.service'; +import { billsService, isDraftInvoice, type InvoiceStatus, type InvoiceSort } from '../../../services/bills.service'; import { Button, Card, Input, Loading, LocalizedDateInput, SortableHeader, useColumnSort, type SortColumnMap } from '../../../components/common'; import { formatMoney } from '../../../components/admin/LineItemsTable'; import { customerAdminService } from '../../../services/customerAdmin.service'; @@ -189,10 +189,10 @@ export const BillsListPage: React.FC = () => { {formatMoney(Number(inv.totalAmountMinor) / 100, inv.currency)} - {inv.isMonthlyDraft ? ( - // Running accumulator draft (manual/monthly). It carries - // status 'scheduled' but never auto-sends on manual cadence, - // so badge it honestly as "Draft" rather than "Scheduled". + {isDraftInvoice(inv) ? ( + // Held invoice: 'scheduled' with no send date (incl. the + // monthly/manual accumulator) never auto-ships, so badge it + // honestly as "Draft" rather than "Scheduled". {t('bills.status.draft', 'Draft')} diff --git a/frontend/src/services/bills.service.ts b/frontend/src/services/bills.service.ts index d64cb3c8..7716f57c 100644 --- a/frontend/src/services/bills.service.ts +++ b/frontend/src/services/bills.service.ts @@ -98,6 +98,20 @@ export interface InvoiceSummary { isMonthlyDraft?: boolean; } +/** + * A "scheduled" invoice with no send date is HELD — the scheduler only + * picks up rows whose `scheduled_send_at <= now`, so a null send date + * means it never auto-ships and is waiting on the admin (Send now / + * Trigger invoice now). Those, plus monthly/manual accumulators, read as + * "Draft" everywhere instead of the misleading "Scheduled". A scheduled + * invoice WITH a future send date is genuinely scheduled and keeps that + * label. + */ +export function isDraftInvoice(inv: Pick): boolean { + if (inv.isMonthlyDraft) return true; + return inv.status === 'scheduled' && !inv.scheduledSendAt; +} + export interface InvoiceDetail extends InvoiceSummary { netAmountMinor: number; vatRate: number | null;