diff --git a/apps/portal/composables/useMrrTrendline.ts b/apps/portal/composables/useMrrTrendline.ts new file mode 100644 index 0000000..c59aa2d --- /dev/null +++ b/apps/portal/composables/useMrrTrendline.ts @@ -0,0 +1,15 @@ +// Decorative MRR trendline for the partner dashboard + reports sparklines. +// +// Historical MRR isn't stored yet (that needs a daily MRR-snapshot job, same +// future work as the operator metrics pipeline), so the sparkline shape is +// PURELY VISUAL — a deterministic ease-out ramp with a gentle wave so it reads +// as organic growth. It is not real data and must not be presented as such; +// when a real series lands, swap this for the snapshot query. Deterministic +// (no Math.random) so SSR and client renders match. +export function useMrrTrendline(points = 90): number[] { + return Array.from({ length: points }, (_, i) => { + const t = i / (points - 1) + const eased = 1 - (1 - t) ** 2 + return Math.round(38000 + eased * 17000 + Math.sin(i / 7) * 320) + }) +} diff --git a/apps/portal/data/customers.ts b/apps/portal/data/customers.ts deleted file mode 100644 index 6efd918..0000000 --- a/apps/portal/data/customers.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Remaining partner fixtures. `partner` (dashboard fallback identity) and -// `partnerMrrSparkline` (decorative dashboard/reports sparkline) are the last -// two holdouts — kept until the dashboard partner header and a real historical -// MRR series replace them. The customer list now comes from /api/partner/tenants. - -export const partner = { - id: 'p-nordicmsp', - name: 'NordicMSP', - domain: 'nordicmsp.dk', - contact: 'Anne Baslund', - email: 'partners@nordicmsp.dk', - marginPct: 20, - customers: 8, - brandColor: '#3F6BFF', - founded: '2024', -} - -// 90-day MRR sparkline · matches the synthetic generator at partner-screens.jsx:198. -// Deterministic seeded values (no Math.random calls each render). -export const partnerMrrSparkline = [ - 38580, 38980, 39520, 40180, 40720, 41080, 41420, 41820, 42100, 42460, - 42820, 43240, 43620, 43980, 44320, 44660, 44960, 45280, 45680, 46020, - 46300, 46580, 46880, 47220, 47540, 47820, 48160, 48420, 48760, 49080, - 49420, 49680, 50020, 50360, 50640, 50920, 51180, 51460, 51720, 51980, - 52220, 52480, 52720, 52960, 53180, 53420, 53620, 53860, 54080, 54260, - 54440, 54620, 54780, 54940, 55080, 55180, 55280, 55360, 55440, 55510, - 55570, 55610, 55650, 55670, 55690, 55700, 55710, 55720, 55730, 55740, - 55745, 55748, 55750, 55750, 55750, 55750, 55750, 55750, 55750, 55750, - 55750, 55750, 55750, 55750, 55750, 55750, 55750, 55750, 55750, 55750, -] diff --git a/apps/portal/pages/partner/index.vue b/apps/portal/pages/partner/index.vue index 9859df8..063d742 100644 --- a/apps/portal/pages/partner/index.vue +++ b/apps/portal/pages/partner/index.vue @@ -6,18 +6,16 @@ -import { partnerMrrSparkline, partner as fixturePartner } from '~/data/customers' import type { CustomerOrg } from '~/types/partner' const toast = useToast() const router = useRouter() const partnerMode = usePartnerMode() -// Real partner identity from /api/me; falls back to the fixture if the user -// somehow lands here without partner data (middleware should've redirected -// them, but defending the read keeps the page from crashing). +// Real partner identity from /api/me. Middleware redirects non-partner users +// away, but we default the name defensively so the header never crashes. const { partner: realPartner } = useMe() -const partner = computed(() => realPartner.value ?? fixturePartner) +const partnerName = computed(() => realPartner.value?.name ?? 'Partner') const wizardOpen = ref(false) const entryCustomer = ref(null) @@ -102,12 +100,9 @@ const newUsers30d = computed( const customersDelta = computed(() => (newCustomers30d.value > 0 ? `+${newCustomers30d.value} / 30d` : '')) const usersDelta = computed(() => (newUsers30d.value > 0 ? `+${newUsers30d.value} / 30d` : '')) -// Sparkline is still fixture-driven — historical MRR isn't stored yet, so -// the chart shape is decorative. Keep it for the design until we wire a -// daily MRR snapshot job. -const sparkline = partnerMrrSparkline -const sparkLast = sparkline[sparkline.length - 1] -const sparkTrendPct = '18.2' // matches source label +// Decorative MRR sparkline shape — historical MRR isn't stored yet (see +// useMrrTrendline). Purely visual until a daily MRR-snapshot job exists. +const sparkline = useMrrTrendline() // Attention list — derived from real tenant state (no fixtures). Surfaces // suspended customers, provisioning errors, seat pressure, and pending/trial @@ -270,7 +265,7 @@ function provisioned() {