Seth commited on
Commit ·
73f93b9
1
Parent(s): 1a77d3d
update
Browse files
frontend/src/pages/SalesDashboard.jsx
CHANGED
|
@@ -599,10 +599,25 @@ export default function SalesDashboard() {
|
|
| 599 |
? 'This year'
|
| 600 |
: 'All time';
|
| 601 |
|
| 602 |
-
|
|
|
|
| 603 |
const qs = quarterlyBoard?.quarters || [];
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
}, [quarterlyBoard]);
|
| 607 |
|
| 608 |
const dateRangeSub =
|
|
@@ -673,7 +688,7 @@ export default function SalesDashboard() {
|
|
| 673 |
<Select value={dashboardPeriod} onValueChange={setDashboardPeriod}>
|
| 674 |
<SelectTrigger
|
| 675 |
className="h-9 w-[min(100%,10.5rem)] border-slate-200 bg-white text-sm shadow-sm"
|
| 676 |
-
aria-label="Date range
|
| 677 |
>
|
| 678 |
<span className="truncate">{dashboardPeriodLabel}</span>
|
| 679 |
</SelectTrigger>
|
|
@@ -717,50 +732,53 @@ export default function SalesDashboard() {
|
|
| 717 |
<Loader2 className="h-5 w-5 animate-spin shrink-0" aria-hidden />
|
| 718 |
Loading quarterly metrics…
|
| 719 |
</div>
|
| 720 |
-
) :
|
| 721 |
<div className="space-y-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
| 723 |
<CardShell
|
| 724 |
title="Annual recurring revenue (ARR)"
|
| 725 |
-
hint="USD ($).
|
| 726 |
className="min-h-0"
|
| 727 |
>
|
| 728 |
<div className="overflow-x-auto -m-1 p-1">
|
| 729 |
-
<table className="w-full min-w-[
|
| 730 |
<thead>
|
| 731 |
<tr className="border-b border-slate-200 text-left text-xs font-semibold text-slate-600">
|
| 732 |
<th className="py-2 pr-3"> </th>
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
</th>
|
| 737 |
-
))}
|
| 738 |
</tr>
|
| 739 |
</thead>
|
| 740 |
<tbody className="tabular-nums">
|
| 741 |
<tr className="border-b border-slate-100">
|
| 742 |
<td className="py-2 pr-3 font-medium text-slate-800">TOTAL</td>
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
</td>
|
| 747 |
-
))}
|
| 748 |
</tr>
|
| 749 |
<tr className="border-b border-slate-100">
|
| 750 |
<td className="py-2 pr-3 text-slate-600">EXISTING</td>
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
</td>
|
| 755 |
-
))}
|
| 756 |
</tr>
|
| 757 |
<tr>
|
| 758 |
<td className="py-2 pr-3 text-slate-600">NEW</td>
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
</td>
|
| 763 |
-
))}
|
| 764 |
</tr>
|
| 765 |
</tbody>
|
| 766 |
</table>
|
|
@@ -768,45 +786,37 @@ export default function SalesDashboard() {
|
|
| 768 |
</CardShell>
|
| 769 |
<CardShell
|
| 770 |
title="Recurring contracts"
|
| 771 |
-
hint="Won deals with at least one recurring PO line
|
| 772 |
className="min-h-0"
|
| 773 |
>
|
| 774 |
<div className="overflow-x-auto -m-1 p-1">
|
| 775 |
-
<table className="w-full min-w-[
|
| 776 |
<thead>
|
| 777 |
<tr className="border-b border-slate-200 text-left text-xs font-semibold text-slate-600">
|
| 778 |
<th className="py-2 pr-3"> </th>
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
</th>
|
| 783 |
-
))}
|
| 784 |
</tr>
|
| 785 |
</thead>
|
| 786 |
<tbody className="tabular-nums">
|
| 787 |
<tr className="border-b border-slate-100">
|
| 788 |
<td className="py-2 pr-3 font-medium text-slate-800">TOTAL</td>
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
</td>
|
| 793 |
-
))}
|
| 794 |
</tr>
|
| 795 |
<tr className="border-b border-slate-100">
|
| 796 |
<td className="py-2 pr-3 text-slate-600">EXISTING</td>
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
</td>
|
| 801 |
-
))}
|
| 802 |
</tr>
|
| 803 |
<tr>
|
| 804 |
<td className="py-2 pr-3 text-slate-600">NEW</td>
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
</td>
|
| 809 |
-
))}
|
| 810 |
</tr>
|
| 811 |
</tbody>
|
| 812 |
</table>
|
|
@@ -815,56 +825,51 @@ export default function SalesDashboard() {
|
|
| 815 |
</div>
|
| 816 |
<CardShell
|
| 817 |
title="One-time revenue"
|
| 818 |
-
hint="
|
| 819 |
className="min-h-0"
|
| 820 |
>
|
| 821 |
<div className="overflow-x-auto -m-1 p-1">
|
| 822 |
-
<table className="w-full min-w-[
|
| 823 |
<thead>
|
| 824 |
<tr className="border-b border-slate-200 text-left text-xs font-semibold text-slate-600">
|
| 825 |
-
<th className="py-2 pr-3">
|
| 826 |
<th className="py-2 text-right">One-time ($)</th>
|
| 827 |
</tr>
|
| 828 |
</thead>
|
| 829 |
<tbody className="tabular-nums">
|
| 830 |
-
|
| 831 |
-
<
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
</tr>
|
| 837 |
-
))}
|
| 838 |
</tbody>
|
| 839 |
</table>
|
| 840 |
</div>
|
| 841 |
-
{
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
</div>
|
| 866 |
-
);
|
| 867 |
-
})()}
|
| 868 |
</CardShell>
|
| 869 |
</div>
|
| 870 |
) : (
|
|
|
|
| 599 |
? 'This year'
|
| 600 |
: 'All time';
|
| 601 |
|
| 602 |
+
/** Latest roll-forward row from API + merged one-time (calendar buckets); UI labels column by date filter, not Q2 2026. */
|
| 603 |
+
const revenueBoardSnapshot = useMemo(() => {
|
| 604 |
const qs = quarterlyBoard?.quarters || [];
|
| 605 |
+
const cols = qs.length <= 6 ? qs : qs.slice(-6);
|
| 606 |
+
const row = cols.length ? cols[cols.length - 1] : null;
|
| 607 |
+
let oneTimeSum = 0;
|
| 608 |
+
const mergedDeals = [];
|
| 609 |
+
const seen = new Set();
|
| 610 |
+
for (const q of cols) {
|
| 611 |
+
oneTimeSum += Number(q.one_time_usd_quarter) || 0;
|
| 612 |
+
for (const d of q.one_time_deals || []) {
|
| 613 |
+
const id = d.deal_id;
|
| 614 |
+
if (id != null && !seen.has(id)) {
|
| 615 |
+
seen.add(id);
|
| 616 |
+
mergedDeals.push(d);
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
return { row, oneTimeSum, mergedDeals, hasData: Boolean(row) };
|
| 621 |
}, [quarterlyBoard]);
|
| 622 |
|
| 623 |
const dateRangeSub =
|
|
|
|
| 688 |
<Select value={dashboardPeriod} onValueChange={setDashboardPeriod}>
|
| 689 |
<SelectTrigger
|
| 690 |
className="h-9 w-[min(100%,10.5rem)] border-slate-200 bg-white text-sm shadow-sm"
|
| 691 |
+
aria-label="Date range for dashboard"
|
| 692 |
>
|
| 693 |
<span className="truncate">{dashboardPeriodLabel}</span>
|
| 694 |
</SelectTrigger>
|
|
|
|
| 732 |
<Loader2 className="h-5 w-5 animate-spin shrink-0" aria-hidden />
|
| 733 |
Loading quarterly metrics…
|
| 734 |
</div>
|
| 735 |
+
) : revenueBoardSnapshot.hasData ? (
|
| 736 |
<div className="space-y-4">
|
| 737 |
+
<p className="text-xs text-slate-500">
|
| 738 |
+
Values reflect <strong className="font-medium text-slate-700">{dashboardPeriodLabel}</strong>
|
| 739 |
+
{isAdmin && ownerFilter !== 'all' ? (
|
| 740 |
+
<>
|
| 741 |
+
{' '}
|
| 742 |
+
· <strong className="font-medium text-slate-700">{ownerFilterLabel}</strong>
|
| 743 |
+
</>
|
| 744 |
+
) : null}
|
| 745 |
+
. ARR roll-forward row is the latest period in range; one-time total sums wins across that
|
| 746 |
+
range.
|
| 747 |
+
</p>
|
| 748 |
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
| 749 |
<CardShell
|
| 750 |
title="Annual recurring revenue (ARR)"
|
| 751 |
+
hint="USD ($). PO lines: monthly ×12, quarterly ×4, annual ×1; one-time lines excluded. Column matches your date + team filters."
|
| 752 |
className="min-h-0"
|
| 753 |
>
|
| 754 |
<div className="overflow-x-auto -m-1 p-1">
|
| 755 |
+
<table className="w-full min-w-[260px] text-sm">
|
| 756 |
<thead>
|
| 757 |
<tr className="border-b border-slate-200 text-left text-xs font-semibold text-slate-600">
|
| 758 |
<th className="py-2 pr-3"> </th>
|
| 759 |
+
<th className="px-2 py-2 text-right whitespace-nowrap">
|
| 760 |
+
{dashboardPeriodLabel}
|
| 761 |
+
</th>
|
|
|
|
|
|
|
| 762 |
</tr>
|
| 763 |
</thead>
|
| 764 |
<tbody className="tabular-nums">
|
| 765 |
<tr className="border-b border-slate-100">
|
| 766 |
<td className="py-2 pr-3 font-medium text-slate-800">TOTAL</td>
|
| 767 |
+
<td className="px-2 py-2 text-right font-semibold">
|
| 768 |
+
{fmtMoney(revenueBoardSnapshot.row?.arr?.total)}
|
| 769 |
+
</td>
|
|
|
|
|
|
|
| 770 |
</tr>
|
| 771 |
<tr className="border-b border-slate-100">
|
| 772 |
<td className="py-2 pr-3 text-slate-600">EXISTING</td>
|
| 773 |
+
<td className="px-2 py-2 text-right text-slate-700">
|
| 774 |
+
{fmtMoney(revenueBoardSnapshot.row?.arr?.existing)}
|
| 775 |
+
</td>
|
|
|
|
|
|
|
| 776 |
</tr>
|
| 777 |
<tr>
|
| 778 |
<td className="py-2 pr-3 text-slate-600">NEW</td>
|
| 779 |
+
<td className="px-2 py-2 text-right text-slate-700">
|
| 780 |
+
{fmtMoney(revenueBoardSnapshot.row?.arr?.new)}
|
| 781 |
+
</td>
|
|
|
|
|
|
|
| 782 |
</tr>
|
| 783 |
</tbody>
|
| 784 |
</table>
|
|
|
|
| 786 |
</CardShell>
|
| 787 |
<CardShell
|
| 788 |
title="Recurring contracts"
|
| 789 |
+
hint="Won deals with at least one recurring PO line. Same filters."
|
| 790 |
className="min-h-0"
|
| 791 |
>
|
| 792 |
<div className="overflow-x-auto -m-1 p-1">
|
| 793 |
+
<table className="w-full min-w-[260px] text-sm">
|
| 794 |
<thead>
|
| 795 |
<tr className="border-b border-slate-200 text-left text-xs font-semibold text-slate-600">
|
| 796 |
<th className="py-2 pr-3"> </th>
|
| 797 |
+
<th className="px-2 py-2 text-right whitespace-nowrap">
|
| 798 |
+
{dashboardPeriodLabel}
|
| 799 |
+
</th>
|
|
|
|
|
|
|
| 800 |
</tr>
|
| 801 |
</thead>
|
| 802 |
<tbody className="tabular-nums">
|
| 803 |
<tr className="border-b border-slate-100">
|
| 804 |
<td className="py-2 pr-3 font-medium text-slate-800">TOTAL</td>
|
| 805 |
+
<td className="px-2 py-2 text-right font-semibold">
|
| 806 |
+
{revenueBoardSnapshot.row?.contracts?.total ?? '—'}
|
| 807 |
+
</td>
|
|
|
|
|
|
|
| 808 |
</tr>
|
| 809 |
<tr className="border-b border-slate-100">
|
| 810 |
<td className="py-2 pr-3 text-slate-600">EXISTING</td>
|
| 811 |
+
<td className="px-2 py-2 text-right text-slate-700">
|
| 812 |
+
{revenueBoardSnapshot.row?.contracts?.existing ?? '—'}
|
| 813 |
+
</td>
|
|
|
|
|
|
|
| 814 |
</tr>
|
| 815 |
<tr>
|
| 816 |
<td className="py-2 pr-3 text-slate-600">NEW</td>
|
| 817 |
+
<td className="px-2 py-2 text-right text-slate-700">
|
| 818 |
+
{revenueBoardSnapshot.row?.contracts?.new ?? '—'}
|
| 819 |
+
</td>
|
|
|
|
|
|
|
| 820 |
</tr>
|
| 821 |
</tbody>
|
| 822 |
</table>
|
|
|
|
| 825 |
</div>
|
| 826 |
<CardShell
|
| 827 |
title="One-time revenue"
|
| 828 |
+
hint="PO lines marked One-time; USD ($). Total summed for the filtered date range."
|
| 829 |
className="min-h-0"
|
| 830 |
>
|
| 831 |
<div className="overflow-x-auto -m-1 p-1">
|
| 832 |
+
<table className="w-full min-w-[260px] text-sm">
|
| 833 |
<thead>
|
| 834 |
<tr className="border-b border-slate-200 text-left text-xs font-semibold text-slate-600">
|
| 835 |
+
<th className="py-2 pr-3">Scope</th>
|
| 836 |
<th className="py-2 text-right">One-time ($)</th>
|
| 837 |
</tr>
|
| 838 |
</thead>
|
| 839 |
<tbody className="tabular-nums">
|
| 840 |
+
<tr className="border-b border-slate-100">
|
| 841 |
+
<td className="py-2 pr-3 text-slate-800">{dashboardPeriodLabel}</td>
|
| 842 |
+
<td className="py-2 text-right">
|
| 843 |
+
{fmtMoney(revenueBoardSnapshot.oneTimeSum)}
|
| 844 |
+
</td>
|
| 845 |
+
</tr>
|
|
|
|
|
|
|
| 846 |
</tbody>
|
| 847 |
</table>
|
| 848 |
</div>
|
| 849 |
+
{revenueBoardSnapshot.mergedDeals.length === 0 ? (
|
| 850 |
+
<p className="mt-3 text-xs text-slate-500">
|
| 851 |
+
No one-time revenue in this scope.
|
| 852 |
+
</p>
|
| 853 |
+
) : (
|
| 854 |
+
<div className="mt-4 rounded-lg border border-slate-100 bg-slate-50/80 px-3 py-2 text-xs text-slate-700">
|
| 855 |
+
<p className="font-semibold text-slate-800">
|
| 856 |
+
Detail ({dashboardPeriodLabel}
|
| 857 |
+
{isAdmin && ownerFilter !== 'all'
|
| 858 |
+
? ` · ${ownerFilterLabel}`
|
| 859 |
+
: ''}
|
| 860 |
+
)
|
| 861 |
+
</p>
|
| 862 |
+
<ul className="mt-2 list-disc space-y-1 pl-5">
|
| 863 |
+
{revenueBoardSnapshot.mergedDeals.map((d) => (
|
| 864 |
+
<li key={d.deal_id}>
|
| 865 |
+
<span className="font-medium">{d.name || 'Deal'}</span>
|
| 866 |
+
{' · '}
|
| 867 |
+
{fmtMoney(d.amount_usd)}
|
| 868 |
+
</li>
|
| 869 |
+
))}
|
| 870 |
+
</ul>
|
| 871 |
+
</div>
|
| 872 |
+
)}
|
|
|
|
|
|
|
|
|
|
| 873 |
</CardShell>
|
| 874 |
</div>
|
| 875 |
) : (
|