@@ -523,6 +526,7 @@ export const BillDetailPage: React.FC = () => {
className="w-full px-3 py-2 rounded-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-accent-dark"
>
+
diff --git a/frontend/src/pages/admin/bills/BillsListPage.tsx b/frontend/src/pages/admin/bills/BillsListPage.tsx
index 346b010f..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';
@@ -47,6 +47,9 @@ export const BillsListPage: React.FC = () => {
q: search || undefined,
status: statusFilter.length ? statusFilter : undefined,
unpaidOnly,
+ // Surface the running monthly/manual accumulator drafts here (they're
+ // badged "Draft"); they're hidden from pickers/sub-lists by default.
+ includeDrafts: true,
sort, page, pageSize: 25,
}),
});
@@ -186,14 +189,23 @@ export const BillsListPage: React.FC = () => {
{formatMoney(Number(inv.totalAmountMinor) / 100, inv.currency)}
- {t(`bills.status.${inv.status}`, inv.status)}
+ {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')}
+
+ ) : (
+ {t(`bills.status.${inv.status}`, inv.status)}
+ )}
))}
diff --git a/frontend/src/services/bills.service.ts b/frontend/src/services/bills.service.ts
index ca5f0a9c..7716f57c 100644
--- a/frontend/src/services/bills.service.ts
+++ b/frontend/src/services/bills.service.ts
@@ -92,6 +92,24 @@ export interface InvoiceSummary {
* (migration 111). Hide line-item editing on these rows; the
* uploaded PDF is the source of truth. */
isImported?: boolean;
+ /** True for the running monthly/manual accumulator draft
+ * (migration 128). Carries status 'scheduled' but never auto-sends
+ * (manual) — shown with a "Draft" badge in the list. */
+ 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 {
@@ -225,6 +243,10 @@ export const billsService = {
sort?: InvoiceSort;
page?: number;
pageSize?: number;
+ /** Include the running monthly/manual accumulator drafts that the
+ * main list hides by default (migration 128). Only the Bills list
+ * opts in; pickers/sub-lists leave it off. */
+ includeDrafts?: boolean;
} = {}): Promise {
const { data } = await api.get('/admin/invoices', {
params: {
@@ -378,6 +400,9 @@ export interface CrmOverviewStats {
monthMinor: number;
quarterMinor: number;
yearMinor: number;
+ /** Revenue since Jan 1 of the current year (calendar YTD). The
+ * dashboard's "year" tile toggles between this and yearMinor. */
+ calendarYearMinor: number;
};
outstanding: {
totalMinor: number;