diff --git a/apps/portal/components/CustomerModeBanner.vue b/apps/portal/components/CustomerModeBanner.vue index bebb182..69ebe5b 100644 --- a/apps/portal/components/CustomerModeBanner.vue +++ b/apps/portal/components/CustomerModeBanner.vue @@ -3,13 +3,22 @@ // specific customer org. Distinct color (indigo — partner mode is normal // operating mode, not danger). Persistent until partner exits. -import { customers } from '~/data/customers' +import type { PartnerTenantDoc } from '~/types/partner' const partnerMode = usePartnerMode() const router = useRouter() +const { partner, isPartnerStaff } = useMe() + +// Real tenant list (shared key; gated to partner-staff so the layout-level +// banner doesn't 403 the partner endpoint for other users). +const { data: tenants } = useFetch('/api/partner/tenants', { + key: 'partner-tenants', + default: () => [], + immediate: isPartnerStaff.value, +}) const activeCustomer = computed(() => - customers.find((c) => c.id === partnerMode.activeCustomerId.value) || null, + (tenants.value ?? []).find((c) => c._id === partnerMode.activeCustomerId.value) || null, ) onMounted(() => partnerMode.hydrate()) @@ -25,7 +34,7 @@ function exit() {
Partner view - managing {{ activeCustomer.name }} · actions are attributed to NordicMSP in the customer's audit log + managing {{ activeCustomer.name }} · actions are attributed to {{ partner?.name ?? 'your partner org' }} in the customer's audit log
@@ -89,10 +101,10 @@ const searchValue = ref('')
- NordicMSP · {{ customers.length }} customers + {{ partnerLabel }} · {{ (tenants?.length ?? 0) }} customers
diff --git a/apps/portal/components/partner/InviteTeammateModal.vue b/apps/portal/components/partner/InviteTeammateModal.vue index 1a15dba..b817951 100644 --- a/apps/portal/components/partner/InviteTeammateModal.vue +++ b/apps/portal/components/partner/InviteTeammateModal.vue @@ -3,7 +3,8 @@ // scoping + require-MFA toggle + optional personal note. Invitations expire // after 7 days — the design surfaces that explicitly. -import { customers } from '~/data/customers' +const { tenants } = usePartnerTenants() +const PLAN_LABEL: Record = { mvp: 'Starter', pro: 'Business', enterprise: 'Enterprise' } defineProps<{ open: boolean }>() const emit = defineEmits<{ @@ -115,18 +116,18 @@ function planBadgeTone(p: string) {
- {{ specific.length }} of {{ customers.length }} selected + {{ specific.length }} of {{ tenants.length }} selected
-
-
+
{{ c.name }} - {{ c.planLabel }} + {{ PLAN_LABEL[c.plan ?? 'pro'] }}
diff --git a/apps/portal/composables/usePartnerMode.ts b/apps/portal/composables/usePartnerMode.ts index 3cfac08..c955c9c 100644 --- a/apps/portal/composables/usePartnerMode.ts +++ b/apps/portal/composables/usePartnerMode.ts @@ -4,9 +4,8 @@ // // In real use, every action while in this mode is logged with the partner's // identity (not the customer's) — the design spec is explicit about this for -// trust. For the prototype we just hold the customer id. - -import type { CustomerOrg } from '~/types/partner' +// trust. We hold the active customer's tenant _id (the same key the customers +// page passes to enter()); consumers resolve it against the real tenant list. const activeCustomerId = ref(null) @@ -34,8 +33,5 @@ export const usePartnerMode = () => { enter, exit, hydrate, - setCustomer: (c: CustomerOrg | null) => { - activeCustomerId.value = c?.id ?? null - }, } } diff --git a/apps/portal/data/customers.ts b/apps/portal/data/customers.ts index 860dc35..6efd918 100644 --- a/apps/portal/data/customers.ts +++ b/apps/portal/data/customers.ts @@ -1,10 +1,7 @@ -// Partner-admin portfolio fixtures. The partner (NordicMSP) manages 8 customer -// orgs. Numbers seeded to match partner-screens.jsx (the canonical design -// source) line for line: same customer set, same MRR, seats, status, mark. - -// Types moved to ~/types/partner so the fixture *data* below can be deleted -// page-by-page (as each goes real) without breaking type-only importers. -import type { CustomerOrg } from '~/types/partner' +// 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', @@ -18,19 +15,6 @@ export const partner = { founded: '2024', } -// Customer set mirrors partner-screens.jsx line 16-25 exactly. -// Health values derived from status + seat utilization (lower for past-due / attention). -export const customers: CustomerOrg[] = [ - { id: 'c-acme', name: 'Acme Workspace', domain: 'acme.dk', plan: 'business', planLabel: 'Business', seats: { used: 24, total: 50 }, health: 88, status: 'healthy', mrrDkk: 4840, brandColor: '#3F6BFF', industry: 'SaaS', createdOn: '2026-02-04', since: 'Feb 2026' }, - { id: 'c-bygherre', name: 'Bygherre Cloud', domain: 'bygherre.dk', plan: 'business', planLabel: 'Business', seats: { used: 12, total: 15 }, health: 38, status: 'past_due', mrrDkk: 2940, brandColor: '#E89A1F', industry: 'Construction', createdOn: '2026-03-12', since: 'Mar 2026' }, - { id: 'c-vester', name: 'Vester Foods', domain: 'vesterfoods.dk', plan: 'starter', planLabel: 'Starter', seats: { used: 8, total: 10 }, health: 82, status: 'healthy', mrrDkk: 980, brandColor: '#5B8C5A', industry: 'Food', createdOn: '2026-04-08', since: 'Apr 2026' }, - { id: 'c-aalborg', name: 'Aalborg Logistik', domain: 'aalborg-log.dk', plan: 'enterprise', planLabel: 'Enterprise', seats: { used: 87, total: 100 }, health: 78, status: 'healthy', mrrDkk: 14500, brandColor: '#0A0A0A', industry: 'Logistics', createdOn: '2025-09-04', since: 'Sep 2025' }, - { id: 'c-norrebro', name: 'Nørrebro Studio', domain: 'nbstudio.dk', plan: 'business', planLabel: 'Business', seats: { used: 6, total: 15 }, health: 68, status: 'trial', mrrDkk: 0, brandColor: '#FF6B4A', industry: 'Creative', createdOn: '2026-05-12', since: '12 May 2026' }, - { id: 'c-vsk', name: 'Vestsjælland Kommune', domain: 'vsk.dk', plan: 'enterprise', planLabel: 'Enterprise', seats: { used: 142, total: 200 }, health: 91, status: 'healthy', mrrDkk: 28400, brandColor: '#5B3F7A', industry: 'Public sector', createdOn: '2024-11-20', since: 'Nov 2024' }, - { id: 'c-broson', name: 'Bro & Søn ApS', domain: 'broson.dk', plan: 'starter', planLabel: 'Starter', seats: { used: 4, total: 10 }, health: 86, status: 'healthy', mrrDkk: 490, brandColor: '#3D3D38', industry: 'Retail', createdOn: '2025-06-15', since: 'Jun 2025' }, - { id: 'c-henriksen', name: 'Henriksen Revision', domain: 'h-revision.dk', plan: 'business', planLabel: 'Business', seats: { used: 18, total: 25 }, health: 58, status: 'attention', mrrDkk: 3600, brandColor: '#B85C38', industry: 'Accounting', createdOn: '2026-01-08', since: 'Jan 2026' }, -] - // 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 = [