';
if ( member.orgOwned ) {
h += ``;
@@ -110,6 +118,7 @@ const renderMembers = () => {
h += `
${i18n('teams_member_kind')}
`;
h += `
${i18n('teams_member_state')}
`;
h += `
${i18n('teams_member_since')}
`;
+ h += `
${i18n('teams_member_plan')}
`;
h += `
${i18n('teams_member_actions')}
`;
h += '
';
for ( const member of sortMembers(annotated) ) h += renderMemberRow(member);
diff --git a/src/gui/src/UI/Dashboard/teamsConsole.js b/src/gui/src/UI/Dashboard/teamsConsole.js
index c55fc9553..ba91e1d27 100644
--- a/src/gui/src/UI/Dashboard/teamsConsole.js
+++ b/src/gui/src/UI/Dashboard/teamsConsole.js
@@ -116,6 +116,25 @@ export function membersBillingSummary (annotated) {
return summary;
}
+/**
+ * What plan a row in the accounts table is on.
+ *
+ * `payer` is the owner, who keeps their personal plan. A suspended seat is
+ * `not_billed` -- it stops costing a per-account charge. Everyone else follows
+ * the team: its tier if it bought one, otherwise the reduced free allowance.
+ *
+ * @param {{ orgOwned: boolean, disabled: boolean }} member
+ * @param {{ current: { tier: string, name_en?: string } | null } | null} plan
+ * @returns {{ kind: 'payer'|'not_billed'|'free'|'tier', name?: string }}
+ */
+export function memberPlanLabel (member, plan) {
+ if ( ! member?.orgOwned ) return { kind: 'payer' };
+ if ( member.disabled ) return { kind: 'not_billed' };
+ const current = plan?.current;
+ if ( ! current ) return { kind: 'free' };
+ return { kind: 'tier', name: current.name_en || current.tier };
+}
+
/**
* The i18n key for an audit action, or `null` for one this build does not know
* about — a new backend action must show as itself rather than as nothing.
diff --git a/src/gui/src/UI/Dashboard/teamsConsole.test.js b/src/gui/src/UI/Dashboard/teamsConsole.test.js
index 2e0d830df..a061b5a76 100644
--- a/src/gui/src/UI/Dashboard/teamsConsole.test.js
+++ b/src/gui/src/UI/Dashboard/teamsConsole.test.js
@@ -6,6 +6,7 @@ import {
auditReasonKey,
memberStatesFromAudit,
membersBillingSummary,
+ memberPlanLabel,
sortMembers,
} from './teamsConsole.js';
@@ -165,3 +166,35 @@ describe('sortMembers', () => {
expect(annotated.map(m => m.username)).toEqual(['zoe', 'ann']);
});
});
+
+describe('what plan a row in the accounts table shows', () => {
+ const paid = { current: { tier: 'team-basic', name_en: 'Team Basic' } };
+
+ it('names the team tier for a billed seat', () => {
+ expect(memberPlanLabel({ orgOwned: true, disabled: false }, paid))
+ .toEqual({ kind: 'tier', name: 'Team Basic' });
+ });
+
+ it('falls back to the tier id when the catalogue has no name', () => {
+ expect(memberPlanLabel({ orgOwned: true }, { current: { tier: 'team-pro' } }))
+ .toEqual({ kind: 'tier', name: 'team-pro' });
+ });
+
+ it('says the owner is the payer, not a seat', () => {
+ // They keep their own personal plan; the team tier is not theirs.
+ expect(memberPlanLabel({ orgOwned: false }, paid)).toEqual({ kind: 'payer' });
+ });
+
+ it('says a suspended seat is not billed', () => {
+ // It stops costing a per-account charge, which is what the card counts.
+ expect(memberPlanLabel({ orgOwned: true, disabled: true }, paid))
+ .toEqual({ kind: 'not_billed' });
+ });
+
+ it('says free when the team bought nothing', () => {
+ for (const plan of [null, undefined, { current: null }]) {
+ expect(memberPlanLabel({ orgOwned: true, disabled: false }, plan))
+ .toEqual({ kind: 'free' });
+ }
+ });
+});
diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js
index 3c8f4e217..e5980070d 100644
--- a/src/gui/src/i18n/translations/en.js
+++ b/src/gui/src/i18n/translations/en.js
@@ -543,6 +543,10 @@ const en = {
teams_add_account_email_hint: 'If you add an address, we email the username and temporary password to it. Otherwise the password below is the only copy.',
teams_add_account_hint:
'Puter creates the account and gives you a one-time password to pass on. The username has to be free across all of Puter.',
+ teams_member_plan: 'Plan',
+ teams_member_plan_payer: '— payer',
+ teams_member_plan_not_billed: 'Not billed',
+ teams_member_plan_free: 'Free',
teams_member_kind: 'Kind',
teams_member_state: 'State',
teams_member_since: 'Added',