Improve package usage visibility
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
import { TenantAddonHistoryEntry, getTenantAddonHistory } from '../api';
|
||||
import { getApiErrorMessage } from '../lib/apiError';
|
||||
import { ADMIN_EVENT_VIEW_PATH, adminPath } from '../constants';
|
||||
import { buildPackageUsageMetrics, PackageUsageMetric, usagePercent } from './billingUsage';
|
||||
import { buildPackageUsageMetrics, getUsageState, PackageUsageMetric, usagePercent } from './billingUsage';
|
||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||
import { useAdminTheme } from './theme';
|
||||
import {
|
||||
@@ -154,6 +154,8 @@ export default function MobileBillingPage() {
|
||||
pkg={activePackage}
|
||||
label={t('billing.sections.packages.card.statusActive', 'Aktiv')}
|
||||
isActive
|
||||
onOpenPortal={openPortal}
|
||||
portalBusy={portalBusy}
|
||||
/>
|
||||
) : null}
|
||||
{packages
|
||||
@@ -257,22 +259,46 @@ export default function MobileBillingPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function PackageCard({ pkg, label, isActive = false }: { pkg: TenantPackageSummary; label?: string; isActive?: boolean }) {
|
||||
function PackageCard({
|
||||
pkg,
|
||||
label,
|
||||
isActive = false,
|
||||
onOpenPortal,
|
||||
portalBusy,
|
||||
}: {
|
||||
pkg: TenantPackageSummary;
|
||||
label?: string;
|
||||
isActive?: boolean;
|
||||
onOpenPortal?: () => void;
|
||||
portalBusy?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation('management');
|
||||
const { border, primary, accentSoft, textStrong, muted } = useAdminTheme();
|
||||
const limits = (pkg.package_limits ?? null) as Record<string, unknown> | null;
|
||||
const remaining = pkg.remaining_events ?? (limits?.max_events_per_year as number | undefined) ?? 0;
|
||||
const limitMaxEvents = typeof limits?.max_events_per_year === 'number' ? (limits?.max_events_per_year as number) : null;
|
||||
const remaining = pkg.remaining_events ?? limitMaxEvents ?? 0;
|
||||
const remainingText =
|
||||
remaining === 0
|
||||
? t('mobileBilling.remainingEventsZero', 'No events remaining')
|
||||
: t('mobileBilling.remainingEvents', '{{count}} events', { count: remaining });
|
||||
: limitMaxEvents
|
||||
? t('mobileBilling.remainingEventsOf', '{{remaining}} of {{limit}} events remaining', {
|
||||
remaining,
|
||||
limit: limitMaxEvents,
|
||||
})
|
||||
: t('mobileBilling.remainingEvents', '{{count}} events', { count: remaining });
|
||||
const expires = pkg.expires_at ? formatDate(pkg.expires_at) : null;
|
||||
const usageMetrics = buildPackageUsageMetrics(pkg);
|
||||
const limitEntries = getPackageLimitEntries(limits, t);
|
||||
const usageStates = usageMetrics.map((metric) => getUsageState(metric));
|
||||
const hasUsageWarning = usageStates.some((state) => state === 'warning' || state === 'danger');
|
||||
const isDanger = usageStates.includes('danger');
|
||||
const limitEntries = getPackageLimitEntries(limits, t, {
|
||||
remainingEvents: pkg.remaining_events ?? null,
|
||||
usedEvents: typeof pkg.used_events === 'number' ? pkg.used_events : null,
|
||||
});
|
||||
const featureKeys = collectPackageFeatures(pkg);
|
||||
const eventUsageText = formatEventUsage(
|
||||
typeof pkg.used_events === 'number' ? pkg.used_events : null,
|
||||
typeof limits?.max_events_per_year === 'number' ? (limits?.max_events_per_year as number) : null,
|
||||
limitMaxEvents,
|
||||
t
|
||||
);
|
||||
return (
|
||||
@@ -345,6 +371,18 @@ function PackageCard({ pkg, label, isActive = false }: { pkg: TenantPackageSumma
|
||||
))}
|
||||
</YStack>
|
||||
) : null}
|
||||
{isActive && hasUsageWarning && onOpenPortal ? (
|
||||
<CTAButton
|
||||
label={
|
||||
isDanger
|
||||
? t('mobileBilling.usage.ctaDanger', 'Upgrade package')
|
||||
: t('mobileBilling.usage.ctaWarning', 'Secure more capacity')
|
||||
}
|
||||
onPress={onOpenPortal}
|
||||
disabled={portalBusy}
|
||||
tone={isDanger ? 'danger' : 'primary'}
|
||||
/>
|
||||
) : null}
|
||||
</MobileCard>
|
||||
);
|
||||
}
|
||||
@@ -358,25 +396,38 @@ function renderFeatureBadge(pkg: TenantPackageSummary, t: any, key: string, labe
|
||||
|
||||
function UsageBar({ metric }: { metric: PackageUsageMetric }) {
|
||||
const { t } = useTranslation('management');
|
||||
const { muted, textStrong, border, primary, subtle } = useAdminTheme();
|
||||
const { muted, textStrong, border, primary, subtle, warningText, danger } = useAdminTheme();
|
||||
const labelMap: Record<PackageUsageMetric['key'], string> = {
|
||||
events: t('mobileBilling.usage.events', 'Events'),
|
||||
guests: t('mobileBilling.usage.guests', 'Guests'),
|
||||
photos: t('mobileBilling.usage.photos', 'Photos'),
|
||||
gallery: t('mobileBilling.usage.gallery', 'Gallery days'),
|
||||
};
|
||||
|
||||
if (!metric.limit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = getUsageState(metric);
|
||||
const hasUsage = metric.used !== null;
|
||||
const valueText = hasUsage
|
||||
? t('mobileBilling.usage.value', { used: metric.used, limit: metric.limit })
|
||||
: t('mobileBilling.usage.limit', { limit: metric.limit });
|
||||
const remainingText = metric.remaining !== null
|
||||
? t('mobileBilling.usage.remaining', { count: metric.remaining })
|
||||
? t('mobileBilling.usage.remainingOf', {
|
||||
remaining: metric.remaining,
|
||||
limit: metric.limit,
|
||||
defaultValue: 'Remaining {{remaining}} of {{limit}}',
|
||||
})
|
||||
: null;
|
||||
const fill = usagePercent(metric);
|
||||
const statusLabel =
|
||||
status === 'danger'
|
||||
? t('mobileBilling.usage.statusDanger', 'Limit reached')
|
||||
: status === 'warning'
|
||||
? t('mobileBilling.usage.statusWarning', 'Low')
|
||||
: null;
|
||||
const fillColor = status === 'danger' ? danger : status === 'warning' ? warningText : primary;
|
||||
|
||||
return (
|
||||
<YStack space="$1.5">
|
||||
@@ -384,12 +435,15 @@ function UsageBar({ metric }: { metric: PackageUsageMetric }) {
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{labelMap[metric.key]}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={textStrong} fontWeight="700">
|
||||
{valueText}
|
||||
</Text>
|
||||
<XStack alignItems="center" space="$1.5">
|
||||
{statusLabel ? <PillBadge tone={status === 'danger' ? 'danger' : 'warning'}>{statusLabel}</PillBadge> : null}
|
||||
<Text fontSize="$xs" color={textStrong} fontWeight="700">
|
||||
{valueText}
|
||||
</Text>
|
||||
</XStack>
|
||||
</XStack>
|
||||
<YStack height={6} borderRadius={999} backgroundColor={border} overflow="hidden">
|
||||
<YStack height="100%" width={`${fill}%`} backgroundColor={hasUsage ? primary : subtle} />
|
||||
<YStack height="100%" width={`${fill}%`} backgroundColor={hasUsage ? fillColor : subtle} />
|
||||
</YStack>
|
||||
{remainingText ? (
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
|
||||
Reference in New Issue
Block a user