Files
fotospiel-app/resources/js/admin/mobile/lib/packageSummary.ts
2026-01-12 17:09:37 +01:00

252 lines
6.7 KiB
TypeScript

import type { TenantPackageSummary } from '../../api';
type Translate = any;
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;
if (normalizedRemaining > limit) {
return t('mobileBilling.usage.remaining', {
count: normalizedRemaining,
defaultValue: 'Remaining {{count}}',
});
}
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' });
}