Gate event create FAB by package quota
This commit is contained in:
@@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||
import { PillBadge, CTAButton, FloatingActionButton, SkeletonCard } from './components/Primitives';
|
||||
import { MobileInput } from './components/FormControls';
|
||||
import { getEvents, TenantEvent } from '../api';
|
||||
import { getEvents, getTenantPackagesOverview, TenantEvent, type TenantPackageSummary } from '../api';
|
||||
import { adminPath } from '../constants';
|
||||
import { isAuthError } from '../auth/tokens';
|
||||
import { getApiErrorMessage } from '../lib/apiError';
|
||||
@@ -32,6 +32,11 @@ export default function MobileEventsPage() {
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [statusFilter, setStatusFilter] = React.useState<EventStatusKey>('all');
|
||||
const [packagesOverview, setPackagesOverview] = React.useState<{
|
||||
packages: TenantPackageSummary[];
|
||||
activePackage: TenantPackageSummary | null;
|
||||
} | null>(null);
|
||||
const [packagesLoading, setPackagesLoading] = React.useState(false);
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
const back = useBackNavigation();
|
||||
const { text, muted, subtle, border, primary, danger, surface, surfaceMuted, accentSoft, accent, shadow } = useAdminTheme();
|
||||
@@ -49,6 +54,44 @@ export default function MobileEventsPage() {
|
||||
})();
|
||||
}, [t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isMember) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isActive = true;
|
||||
setPackagesLoading(true);
|
||||
getTenantPackagesOverview()
|
||||
.then((overview) => {
|
||||
if (isActive) {
|
||||
setPackagesOverview(overview);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (isActive) {
|
||||
setPackagesOverview(null);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (isActive) {
|
||||
setPackagesLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, [isMember]);
|
||||
|
||||
const canCreateEvent = React.useMemo(() => {
|
||||
if (isMember || packagesLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activePackages = collectActivePackages(packagesOverview);
|
||||
return activePackages.some((pkg) => packageHasRemainingEvents(pkg));
|
||||
}, [isMember, packagesLoading, packagesOverview]);
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
activeTab="home"
|
||||
@@ -164,7 +207,7 @@ export default function MobileEventsPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isMember ? (
|
||||
{canCreateEvent ? (
|
||||
<FloatingActionButton
|
||||
label={t('events.list.actions.create')}
|
||||
icon={Plus}
|
||||
@@ -476,6 +519,65 @@ function renderName(name: TenantEvent['name'], t: (key: string) => string): stri
|
||||
return t('events.placeholders.untitled');
|
||||
}
|
||||
|
||||
function collectActivePackages(
|
||||
overview: { packages: TenantPackageSummary[]; activePackage: TenantPackageSummary | null } | null
|
||||
): TenantPackageSummary[] {
|
||||
if (!overview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const packages = overview.packages ?? [];
|
||||
const activePackage = overview.activePackage;
|
||||
const activeList = packages.filter((pkg) => pkg.active);
|
||||
|
||||
if (activePackage && !activeList.some((pkg) => pkg.id === activePackage.id) && activePackage.active) {
|
||||
return [activePackage, ...activeList];
|
||||
}
|
||||
|
||||
return activeList;
|
||||
}
|
||||
|
||||
function 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;
|
||||
}
|
||||
|
||||
function packageHasRemainingEvents(pkg: TenantPackageSummary): boolean {
|
||||
const remaining = toNumber(pkg.remaining_events);
|
||||
|
||||
if (pkg.package_type !== 'reseller') {
|
||||
if (remaining !== null) {
|
||||
return remaining > 0;
|
||||
}
|
||||
|
||||
const usedEvents = toNumber(pkg.used_events) ?? 0;
|
||||
return usedEvents < 1;
|
||||
}
|
||||
|
||||
if (remaining !== null) {
|
||||
return remaining > 0;
|
||||
}
|
||||
|
||||
const limits = (pkg.package_limits ?? {}) as Record<string, unknown>;
|
||||
const limitMaxEvents = toNumber(limits.max_events_per_year);
|
||||
if (limitMaxEvents === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const usedEvents = toNumber(pkg.used_events) ?? 0;
|
||||
return limitMaxEvents > usedEvents;
|
||||
}
|
||||
|
||||
function resolveEventSearchName(name: TenantEvent['name'], t: (key: string) => string): string {
|
||||
if (typeof name === 'string') return name;
|
||||
if (name && typeof name === 'object') {
|
||||
|
||||
Reference in New Issue
Block a user