Improve package usage visibility
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-06 14:17:27 +01:00
parent ef1773d966
commit 232302eb6f
12 changed files with 370 additions and 31 deletions

View File

@@ -14,7 +14,8 @@ const t = (key: string, options?: Record<string, unknown> | string) => {
const template = (options?.defaultValue as string | undefined) ?? key;
return template
.replace('{{used}}', String(options?.used ?? '{{used}}'))
.replace('{{limit}}', String(options?.limit ?? '{{limit}}'));
.replace('{{limit}}', String(options?.limit ?? '{{limit}}'))
.replace('{{remaining}}', String(options?.remaining ?? '{{remaining}}'));
};
describe('packageSummary helpers', () => {
@@ -46,10 +47,10 @@ describe('packageSummary helpers', () => {
});
it('returns labeled limit entries', () => {
const result = getPackageLimitEntries({ max_photos: 120 }, t);
const result = getPackageLimitEntries({ max_photos: 120, remaining_photos: 30 }, t);
expect(result[0].label).toBe('Photos');
expect(result[0].value).toBe('120');
expect(result[0].value).toBe('30 of 120 remaining');
});
it('formats event usage copy', () => {

View File

@@ -2,6 +2,11 @@ import type { TenantPackageSummary } from '../../api';
type Translate = (key: string, options?: Record<string, unknown> | string) => string;
type LimitUsageOverrides = {
remainingEvents?: number | null;
usedEvents?: number | null;
};
const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
priority_support: {
key: 'mobileDashboard.packageSummary.feature.priority_support',
@@ -111,6 +116,38 @@ export type PackageLimitEntry = {
value: string;
};
const toNumber = (value: unknown): number | null => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
};
const formatLimitWithRemaining = (limit: number | null, remaining: number | null, t: Translate): string => {
if (limit === null || limit === undefined) {
return t('mobileDashboard.packageSummary.unlimited', 'Unlimited');
}
if (remaining !== null && remaining >= 0) {
const normalizedRemaining = Number.isFinite(remaining) ? Math.max(0, Math.round(remaining)) : remaining;
return t('mobileBilling.usage.remainingOf', {
remaining: normalizedRemaining,
limit,
defaultValue: '{{remaining}} of {{limit}} remaining',
});
}
return String(limit);
};
export function getPackageFeatureLabel(feature: string, t: Translate): string {
const entry = FEATURE_LABELS[feature];
if (entry) {
@@ -128,7 +165,11 @@ export function formatPackageLimit(value: number | null | undefined, t: Translat
return String(value);
}
export function getPackageLimitEntries(limits: Record<string, unknown> | null, t: Translate): PackageLimitEntry[] {
export function getPackageLimitEntries(
limits: Record<string, unknown> | null,
t: Translate,
usageOverrides: LimitUsageOverrides = {}
): PackageLimitEntry[] {
if (!limits) {
return [];
}
@@ -136,10 +177,37 @@ export function getPackageLimitEntries(limits: Record<string, unknown> | null, t
return LIMIT_LABELS.map(({ key, labelKey, fallback }) => ({
key,
label: t(labelKey, fallback),
value: formatPackageLimit((limits as Record<string, number | null>)[key], t),
value: formatLimitWithRemaining(
toNumber((limits as Record<string, number | null>)[key]),
resolveRemainingForKey(limits, key, usageOverrides),
t
),
}));
}
const resolveRemainingForKey = (
limits: Record<string, unknown>,
key: string,
usageOverrides: LimitUsageOverrides
): number | null => {
if (key === 'max_events_per_year') {
return toNumber(usageOverrides.remainingEvents ?? limits.remaining_events);
}
const map: Record<string, string> = {
max_photos: 'remaining_photos',
max_guests: 'remaining_guests',
gallery_days: 'remaining_gallery_days',
};
const remainingKey = map[key];
if (!remainingKey) {
return null;
}
return toNumber(limits[remainingKey]);
};
export function collectPackageFeatures(pkg: TenantPackageSummary): string[] {
const features = new Set<string>();
const direct = Array.isArray(pkg.features) ? pkg.features : [];