Expand package limit and feature details
This commit is contained in:
@@ -2068,6 +2068,7 @@
|
|||||||
"limitPhotos": "Fotos",
|
"limitPhotos": "Fotos",
|
||||||
"limitGuests": "Gäste",
|
"limitGuests": "Gäste",
|
||||||
"limitDays": "Galerietage",
|
"limitDays": "Galerietage",
|
||||||
|
"limitsTitle": "Limits",
|
||||||
"remaining": "Verbleibende Events",
|
"remaining": "Verbleibende Events",
|
||||||
"purchased": "Gekauft",
|
"purchased": "Gekauft",
|
||||||
"expires": "Läuft ab",
|
"expires": "Läuft ab",
|
||||||
@@ -2076,8 +2077,15 @@
|
|||||||
"featuresTitle": "Enthaltene Features",
|
"featuresTitle": "Enthaltene Features",
|
||||||
"feature": {
|
"feature": {
|
||||||
"priority_support": "Priority Support",
|
"priority_support": "Priority Support",
|
||||||
|
"reseller_dashboard": "Reseller-Dashboard",
|
||||||
"custom_domain": "Eigene Domain",
|
"custom_domain": "Eigene Domain",
|
||||||
|
"custom_branding": "Benutzerdefiniertes Branding",
|
||||||
|
"custom_tasks": "Individuelle Aufgaben",
|
||||||
|
"unlimited_sharing": "Unbegrenztes Sharing",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
|
"advanced_reporting": "Erweitertes Reporting",
|
||||||
|
"live_slideshow": "Live-Slideshow",
|
||||||
|
"basic_uploads": "Gäste-Uploads",
|
||||||
"team_management": "Team-Management",
|
"team_management": "Team-Management",
|
||||||
"moderation_tools": "Moderations-Tools",
|
"moderation_tools": "Moderations-Tools",
|
||||||
"prints": "Print-Uploads",
|
"prints": "Print-Uploads",
|
||||||
@@ -2697,7 +2705,12 @@
|
|||||||
"mobileBilling": {
|
"mobileBilling": {
|
||||||
"packageFallback": "Paket",
|
"packageFallback": "Paket",
|
||||||
"remainingEvents": "{{count}} Events",
|
"remainingEvents": "{{count}} Events",
|
||||||
|
"eventsCreated": "{{used}} von {{limit}} Events angelegt",
|
||||||
"openEvent": "Event öffnen",
|
"openEvent": "Event öffnen",
|
||||||
|
"details": {
|
||||||
|
"limitsTitle": "Limits",
|
||||||
|
"featuresTitle": "Features"
|
||||||
|
},
|
||||||
"usage": {
|
"usage": {
|
||||||
"events": "Events",
|
"events": "Events",
|
||||||
"guests": "Gäste",
|
"guests": "Gäste",
|
||||||
@@ -2717,6 +2730,13 @@
|
|||||||
"days": "+{{count}} Tage"
|
"days": "+{{count}} Tage"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packageLimits": {
|
||||||
|
"max_photos": "Fotos",
|
||||||
|
"max_guests": "Gäste",
|
||||||
|
"max_tasks": "Aufgaben",
|
||||||
|
"gallery_days": "Galerietage",
|
||||||
|
"max_events_per_year": "Events pro Jahr"
|
||||||
|
},
|
||||||
"mobileEvents": {
|
"mobileEvents": {
|
||||||
"edit": "Event bearbeiten"
|
"edit": "Event bearbeiten"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2072,6 +2072,7 @@
|
|||||||
"limitPhotos": "Photos",
|
"limitPhotos": "Photos",
|
||||||
"limitGuests": "Guests",
|
"limitGuests": "Guests",
|
||||||
"limitDays": "Gallery days",
|
"limitDays": "Gallery days",
|
||||||
|
"limitsTitle": "Limits",
|
||||||
"remaining": "Remaining events",
|
"remaining": "Remaining events",
|
||||||
"purchased": "Purchased",
|
"purchased": "Purchased",
|
||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
@@ -2080,8 +2081,15 @@
|
|||||||
"featuresTitle": "Included features",
|
"featuresTitle": "Included features",
|
||||||
"feature": {
|
"feature": {
|
||||||
"priority_support": "Priority support",
|
"priority_support": "Priority support",
|
||||||
|
"reseller_dashboard": "Reseller dashboard",
|
||||||
"custom_domain": "Custom domain",
|
"custom_domain": "Custom domain",
|
||||||
|
"custom_branding": "Custom branding",
|
||||||
|
"custom_tasks": "Custom tasks",
|
||||||
|
"unlimited_sharing": "Unlimited sharing",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
|
"advanced_reporting": "Advanced reporting",
|
||||||
|
"live_slideshow": "Live slideshow",
|
||||||
|
"basic_uploads": "Guest uploads",
|
||||||
"team_management": "Team management",
|
"team_management": "Team management",
|
||||||
"moderation_tools": "Moderation tools",
|
"moderation_tools": "Moderation tools",
|
||||||
"prints": "Print uploads",
|
"prints": "Print uploads",
|
||||||
@@ -2701,7 +2709,12 @@
|
|||||||
"mobileBilling": {
|
"mobileBilling": {
|
||||||
"packageFallback": "Package",
|
"packageFallback": "Package",
|
||||||
"remainingEvents": "{{count}} events",
|
"remainingEvents": "{{count}} events",
|
||||||
|
"eventsCreated": "{{used}} of {{limit}} events created",
|
||||||
"openEvent": "Open event",
|
"openEvent": "Open event",
|
||||||
|
"details": {
|
||||||
|
"limitsTitle": "Limits",
|
||||||
|
"featuresTitle": "Features"
|
||||||
|
},
|
||||||
"usage": {
|
"usage": {
|
||||||
"events": "Events",
|
"events": "Events",
|
||||||
"guests": "Guests",
|
"guests": "Guests",
|
||||||
@@ -2721,6 +2734,13 @@
|
|||||||
"days": "+{{count}} days"
|
"days": "+{{count}} days"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packageLimits": {
|
||||||
|
"max_photos": "Photos",
|
||||||
|
"max_guests": "Guests",
|
||||||
|
"max_tasks": "Tasks",
|
||||||
|
"gallery_days": "Gallery days",
|
||||||
|
"max_events_per_year": "Events per year"
|
||||||
|
},
|
||||||
"mobileEvents": {
|
"mobileEvents": {
|
||||||
"edit": "Edit event"
|
"edit": "Edit event"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ import { ADMIN_EVENT_VIEW_PATH, adminPath } from '../constants';
|
|||||||
import { buildPackageUsageMetrics, PackageUsageMetric, usagePercent } from './billingUsage';
|
import { buildPackageUsageMetrics, PackageUsageMetric, usagePercent } from './billingUsage';
|
||||||
import { useBackNavigation } from './hooks/useBackNavigation';
|
import { useBackNavigation } from './hooks/useBackNavigation';
|
||||||
import { useAdminTheme } from './theme';
|
import { useAdminTheme } from './theme';
|
||||||
|
import {
|
||||||
|
collectPackageFeatures,
|
||||||
|
formatEventUsage,
|
||||||
|
getPackageFeatureLabel,
|
||||||
|
getPackageLimitEntries,
|
||||||
|
} from './lib/packageSummary';
|
||||||
|
|
||||||
export default function MobileBillingPage() {
|
export default function MobileBillingPage() {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
@@ -254,9 +260,17 @@ export default function MobileBillingPage() {
|
|||||||
function PackageCard({ pkg, label, isActive = false }: { pkg: TenantPackageSummary; label?: string; isActive?: boolean }) {
|
function PackageCard({ pkg, label, isActive = false }: { pkg: TenantPackageSummary; label?: string; isActive?: boolean }) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const { border, primary, accentSoft, textStrong, muted } = useAdminTheme();
|
const { border, primary, accentSoft, textStrong, muted } = useAdminTheme();
|
||||||
const remaining = pkg.remaining_events ?? (pkg.package_limits?.max_events_per_year as number | undefined) ?? 0;
|
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 expires = pkg.expires_at ? formatDate(pkg.expires_at) : null;
|
const expires = pkg.expires_at ? formatDate(pkg.expires_at) : null;
|
||||||
const usageMetrics = buildPackageUsageMetrics(pkg);
|
const usageMetrics = buildPackageUsageMetrics(pkg);
|
||||||
|
const limitEntries = getPackageLimitEntries(limits, t);
|
||||||
|
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,
|
||||||
|
t
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<MobileCard
|
<MobileCard
|
||||||
borderColor={isActive ? primary : border}
|
borderColor={isActive ? primary : border}
|
||||||
@@ -285,6 +299,43 @@ function PackageCard({ pkg, label, isActive = false }: { pkg: TenantPackageSumma
|
|||||||
{renderFeatureBadge(pkg, t, 'branding_allowed', t('billing.features.branding', 'Branding'))}
|
{renderFeatureBadge(pkg, t, 'branding_allowed', t('billing.features.branding', 'Branding'))}
|
||||||
{renderFeatureBadge(pkg, t, 'watermark_allowed', t('billing.features.watermark', 'Watermark'))}
|
{renderFeatureBadge(pkg, t, 'watermark_allowed', t('billing.features.watermark', 'Watermark'))}
|
||||||
</XStack>
|
</XStack>
|
||||||
|
{eventUsageText ? (
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{eventUsageText}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{limitEntries.length ? (
|
||||||
|
<YStack space="$1.5" marginTop="$2">
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{t('mobileBilling.details.limitsTitle', 'Limits')}
|
||||||
|
</Text>
|
||||||
|
{limitEntries.map((entry) => (
|
||||||
|
<XStack key={entry.key} alignItems="center" justifyContent="space-between">
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{entry.label}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="$xs" color={textStrong} fontWeight="700">
|
||||||
|
{entry.value}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
))}
|
||||||
|
</YStack>
|
||||||
|
) : null}
|
||||||
|
{featureKeys.length ? (
|
||||||
|
<YStack space="$1.5" marginTop="$2">
|
||||||
|
<Text fontSize="$xs" color={muted}>
|
||||||
|
{t('mobileBilling.details.featuresTitle', 'Features')}
|
||||||
|
</Text>
|
||||||
|
{featureKeys.map((feature) => (
|
||||||
|
<XStack key={feature} alignItems="center" space="$2">
|
||||||
|
<Sparkles size={14} color={primary} />
|
||||||
|
<Text fontSize="$xs" color={textStrong}>
|
||||||
|
{getPackageFeatureLabel(feature, t)}
|
||||||
|
</Text>
|
||||||
|
</XStack>
|
||||||
|
))}
|
||||||
|
</YStack>
|
||||||
|
) : null}
|
||||||
{usageMetrics.length ? (
|
{usageMetrics.length ? (
|
||||||
<YStack space="$2" marginTop="$2">
|
<YStack space="$2" marginTop="$2">
|
||||||
{usageMetrics.map((metric) => (
|
{usageMetrics.map((metric) => (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
|
|||||||
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
import { useDevicePermissions } from './hooks/useDevicePermissions';
|
||||||
import { useInstallPrompt } from './hooks/useInstallPrompt';
|
import { useInstallPrompt } from './hooks/useInstallPrompt';
|
||||||
import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from './lib/mobileTour';
|
import { getTourSeen, resolveTourStepKeys, setTourSeen, type TourStepKey } from './lib/mobileTour';
|
||||||
import { formatPackageLimit, getPackageFeatureLabel } from './lib/packageSummary';
|
import { collectPackageFeatures, formatPackageLimit, getPackageFeatureLabel, getPackageLimitEntries } from './lib/packageSummary';
|
||||||
import { trackOnboarding } from '../api';
|
import { trackOnboarding } from '../api';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
import { ADMIN_ACTION_COLORS, ADMIN_MOTION, useAdminTheme } from './theme';
|
||||||
@@ -508,7 +508,14 @@ function PackageSummarySheet({
|
|||||||
const maxPhotos = (limits as Record<string, number | null> | null)?.max_photos ?? null;
|
const maxPhotos = (limits as Record<string, number | null> | null)?.max_photos ?? null;
|
||||||
const maxGuests = (limits as Record<string, number | null> | null)?.max_guests ?? null;
|
const maxGuests = (limits as Record<string, number | null> | null)?.max_guests ?? null;
|
||||||
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
|
const galleryDays = (limits as Record<string, number | null> | null)?.gallery_days ?? null;
|
||||||
const hasFeatures = Array.isArray(features) && features.length > 0;
|
const resolvedFeatures = collectPackageFeatures({
|
||||||
|
features,
|
||||||
|
package_limits: limits,
|
||||||
|
branding_allowed: (limits as any)?.branding_allowed ?? null,
|
||||||
|
watermark_allowed: (limits as any)?.watermark_allowed ?? null,
|
||||||
|
} as any);
|
||||||
|
const limitEntries = getPackageLimitEntries(limits, t);
|
||||||
|
const hasFeatures = resolvedFeatures.length > 0;
|
||||||
|
|
||||||
const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown');
|
const formatDate = (value?: string | null) => formatEventDate(value, locale) ?? t('mobileDashboard.packageSummary.unknown', 'Unknown');
|
||||||
|
|
||||||
@@ -539,13 +546,26 @@ function PackageSummarySheet({
|
|||||||
</YStack>
|
</YStack>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
|
|
||||||
{hasFeatures ? (
|
{limitEntries.length ? (
|
||||||
|
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||||
|
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||||
|
{t('mobileDashboard.packageSummary.limitsTitle', 'Limits')}
|
||||||
|
</Text>
|
||||||
|
<YStack space="$1.5" marginTop="$2">
|
||||||
|
{limitEntries.map((entry) => (
|
||||||
|
<SummaryRow key={entry.key} label={entry.label} value={entry.value} />
|
||||||
|
))}
|
||||||
|
</YStack>
|
||||||
|
</MobileCard>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{hasFeatures ? (
|
||||||
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
<MobileCard space="$2" borderColor={border} backgroundColor={surface}>
|
||||||
<Text fontSize="$sm" fontWeight="800" color={text}>
|
<Text fontSize="$sm" fontWeight="800" color={text}>
|
||||||
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
|
{t('mobileDashboard.packageSummary.featuresTitle', 'Included features')}
|
||||||
</Text>
|
</Text>
|
||||||
<YStack space="$1.5" marginTop="$2">
|
<YStack space="$1.5" marginTop="$2">
|
||||||
{features?.map((feature) => (
|
{resolvedFeatures.map((feature) => (
|
||||||
<XStack key={feature} alignItems="center" space="$2">
|
<XStack key={feature} alignItems="center" space="$2">
|
||||||
<XStack width={24} height={24} borderRadius={8} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
|
<XStack width={24} height={24} borderRadius={8} backgroundColor={accentSoft} alignItems="center" justifyContent="center">
|
||||||
<Sparkles size={14} color={primary} />
|
<Sparkles size={14} color={primary} />
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { formatPackageLimit, getPackageFeatureLabel } from './packageSummary';
|
import {
|
||||||
|
collectPackageFeatures,
|
||||||
|
formatEventUsage,
|
||||||
|
formatPackageLimit,
|
||||||
|
getPackageFeatureLabel,
|
||||||
|
getPackageLimitEntries,
|
||||||
|
} from './packageSummary';
|
||||||
|
|
||||||
const t = (key: string, options?: Record<string, unknown> | string) => {
|
const t = (key: string, options?: Record<string, unknown> | string) => {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
return (options?.defaultValue as string | undefined) ?? key;
|
const template = (options?.defaultValue as string | undefined) ?? key;
|
||||||
|
return template
|
||||||
|
.replace('{{used}}', String(options?.used ?? '{{used}}'))
|
||||||
|
.replace('{{limit}}', String(options?.limit ?? '{{limit}}'));
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('packageSummary helpers', () => {
|
describe('packageSummary helpers', () => {
|
||||||
@@ -24,4 +33,28 @@ describe('packageSummary helpers', () => {
|
|||||||
it('formats numeric package limits', () => {
|
it('formats numeric package limits', () => {
|
||||||
expect(formatPackageLimit(12, t)).toBe('12');
|
expect(formatPackageLimit(12, t)).toBe('12');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('collects features from package and limit payloads', () => {
|
||||||
|
const result = collectPackageFeatures({
|
||||||
|
features: ['custom_branding'],
|
||||||
|
package_limits: { features: ['reseller_dashboard'] },
|
||||||
|
branding_allowed: true,
|
||||||
|
watermark_allowed: false,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.arrayContaining(['custom_branding', 'reseller_dashboard', 'branding_allowed']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns labeled limit entries', () => {
|
||||||
|
const result = getPackageLimitEntries({ max_photos: 120 }, t);
|
||||||
|
|
||||||
|
expect(result[0].label).toBe('Photos');
|
||||||
|
expect(result[0].value).toBe('120');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats event usage copy', () => {
|
||||||
|
const result = formatEventUsage(3, 10, t);
|
||||||
|
|
||||||
|
expect(result).toBe('3 of 10 events created');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { TenantPackageSummary } from '../../api';
|
||||||
|
|
||||||
type Translate = (key: string, options?: Record<string, unknown> | string) => string;
|
type Translate = (key: string, options?: Record<string, unknown> | string) => string;
|
||||||
|
|
||||||
const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
|
const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
|
||||||
@@ -5,14 +7,42 @@ const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
|
|||||||
key: 'mobileDashboard.packageSummary.feature.priority_support',
|
key: 'mobileDashboard.packageSummary.feature.priority_support',
|
||||||
fallback: 'Priority support',
|
fallback: 'Priority support',
|
||||||
},
|
},
|
||||||
|
reseller_dashboard: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.reseller_dashboard',
|
||||||
|
fallback: 'Reseller dashboard',
|
||||||
|
},
|
||||||
custom_domain: {
|
custom_domain: {
|
||||||
key: 'mobileDashboard.packageSummary.feature.custom_domain',
|
key: 'mobileDashboard.packageSummary.feature.custom_domain',
|
||||||
fallback: 'Custom domain',
|
fallback: 'Custom domain',
|
||||||
},
|
},
|
||||||
|
custom_branding: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.custom_branding',
|
||||||
|
fallback: 'Custom branding',
|
||||||
|
},
|
||||||
|
custom_tasks: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.custom_tasks',
|
||||||
|
fallback: 'Custom tasks',
|
||||||
|
},
|
||||||
|
unlimited_sharing: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.unlimited_sharing',
|
||||||
|
fallback: 'Unlimited sharing',
|
||||||
|
},
|
||||||
analytics: {
|
analytics: {
|
||||||
key: 'mobileDashboard.packageSummary.feature.analytics',
|
key: 'mobileDashboard.packageSummary.feature.analytics',
|
||||||
fallback: 'Analytics',
|
fallback: 'Analytics',
|
||||||
},
|
},
|
||||||
|
advanced_reporting: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.advanced_reporting',
|
||||||
|
fallback: 'Advanced reporting',
|
||||||
|
},
|
||||||
|
live_slideshow: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.live_slideshow',
|
||||||
|
fallback: 'Live slideshow',
|
||||||
|
},
|
||||||
|
basic_uploads: {
|
||||||
|
key: 'mobileDashboard.packageSummary.feature.basic_uploads',
|
||||||
|
fallback: 'Guest uploads',
|
||||||
|
},
|
||||||
team_management: {
|
team_management: {
|
||||||
key: 'mobileDashboard.packageSummary.feature.team_management',
|
key: 'mobileDashboard.packageSummary.feature.team_management',
|
||||||
fallback: 'Team management',
|
fallback: 'Team management',
|
||||||
@@ -47,6 +77,40 @@ const FEATURE_LABELS: Record<string, { key: string; fallback: string }> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LIMIT_LABELS: Array<{ key: string; labelKey: string; fallback: string }> = [
|
||||||
|
{
|
||||||
|
key: 'max_photos',
|
||||||
|
labelKey: 'packageLimits.max_photos',
|
||||||
|
fallback: 'Photos',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'max_guests',
|
||||||
|
labelKey: 'packageLimits.max_guests',
|
||||||
|
fallback: 'Guests',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'max_tasks',
|
||||||
|
labelKey: 'packageLimits.max_tasks',
|
||||||
|
fallback: 'Tasks',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gallery_days',
|
||||||
|
labelKey: 'packageLimits.gallery_days',
|
||||||
|
fallback: 'Gallery days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'max_events_per_year',
|
||||||
|
labelKey: 'packageLimits.max_events_per_year',
|
||||||
|
fallback: 'Events per year',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type PackageLimitEntry = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export function getPackageFeatureLabel(feature: string, t: Translate): string {
|
export function getPackageFeatureLabel(feature: string, t: Translate): string {
|
||||||
const entry = FEATURE_LABELS[feature];
|
const entry = FEATURE_LABELS[feature];
|
||||||
if (entry) {
|
if (entry) {
|
||||||
@@ -63,3 +127,51 @@ export function formatPackageLimit(value: number | null | undefined, t: Translat
|
|||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPackageLimitEntries(limits: Record<string, unknown> | null, t: Translate): PackageLimitEntry[] {
|
||||||
|
if (!limits) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return LIMIT_LABELS.map(({ key, labelKey, fallback }) => ({
|
||||||
|
key,
|
||||||
|
label: t(labelKey, fallback),
|
||||||
|
value: formatPackageLimit((limits as Record<string, number | null>)[key], t),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectPackageFeatures(pkg: TenantPackageSummary): string[] {
|
||||||
|
const features = new Set<string>();
|
||||||
|
const direct = Array.isArray(pkg.features) ? pkg.features : [];
|
||||||
|
const limitFeatures = Array.isArray((pkg.package_limits as any)?.features) ? (pkg.package_limits as any).features : [];
|
||||||
|
|
||||||
|
direct.forEach((feature) => {
|
||||||
|
if (typeof feature === 'string' && feature.trim()) {
|
||||||
|
features.add(feature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
limitFeatures.forEach((feature: unknown) => {
|
||||||
|
if (typeof feature === 'string' && feature.trim()) {
|
||||||
|
features.add(feature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pkg.branding_allowed) {
|
||||||
|
features.add('branding_allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pkg.watermark_allowed) {
|
||||||
|
features.add('watermark_allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(features);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatEventUsage(used: number | null, limit: number | null, t: Translate): string | null {
|
||||||
|
if (limit === null || used === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return t('mobileBilling.eventsCreated', { used, limit, defaultValue: '{{used}} of {{limit}} events created' });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user