246 lines
6.6 KiB
TypeScript
246 lines
6.6 KiB
TypeScript
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',
|
|
fallback: 'Priority support',
|
|
},
|
|
reseller_dashboard: {
|
|
key: 'mobileDashboard.packageSummary.feature.reseller_dashboard',
|
|
fallback: 'Reseller dashboard',
|
|
},
|
|
custom_domain: {
|
|
key: 'mobileDashboard.packageSummary.feature.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: {
|
|
key: 'mobileDashboard.packageSummary.feature.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: {
|
|
key: 'mobileDashboard.packageSummary.feature.team_management',
|
|
fallback: 'Team management',
|
|
},
|
|
moderation_tools: {
|
|
key: 'mobileDashboard.packageSummary.feature.moderation_tools',
|
|
fallback: 'Moderation tools',
|
|
},
|
|
prints: {
|
|
key: 'mobileDashboard.packageSummary.feature.prints',
|
|
fallback: 'Print uploads',
|
|
},
|
|
photo_likes_enabled: {
|
|
key: 'mobileDashboard.packageSummary.feature.photo_likes_enabled',
|
|
fallback: 'Photo likes',
|
|
},
|
|
event_checklist: {
|
|
key: 'mobileDashboard.packageSummary.feature.event_checklist',
|
|
fallback: 'Event checklist',
|
|
},
|
|
advanced_analytics: {
|
|
key: 'mobileDashboard.packageSummary.feature.advanced_analytics',
|
|
fallback: 'Advanced analytics',
|
|
},
|
|
branding_allowed: {
|
|
key: 'mobileDashboard.packageSummary.feature.branding_allowed',
|
|
fallback: 'Branding',
|
|
},
|
|
watermark_allowed: {
|
|
key: 'mobileDashboard.packageSummary.feature.watermark_allowed',
|
|
fallback: 'Watermarks',
|
|
},
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
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) {
|
|
return t(entry.key, entry.fallback);
|
|
}
|
|
|
|
return t(`mobileDashboard.packageSummary.feature.${feature}`, feature);
|
|
}
|
|
|
|
export function formatPackageLimit(value: number | null | undefined, t: Translate): string {
|
|
if (value === null || value === undefined) {
|
|
return t('mobileDashboard.packageSummary.unlimited', 'Unlimited');
|
|
}
|
|
|
|
return String(value);
|
|
}
|
|
|
|
export function getPackageLimitEntries(
|
|
limits: Record<string, unknown> | null,
|
|
t: Translate,
|
|
usageOverrides: LimitUsageOverrides = {}
|
|
): PackageLimitEntry[] {
|
|
if (!limits) {
|
|
return [];
|
|
}
|
|
|
|
return LIMIT_LABELS.map(({ key, labelKey, fallback }) => ({
|
|
key,
|
|
label: t(labelKey, fallback),
|
|
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 : [];
|
|
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' });
|
|
}
|