Fix demo task readiness and gate event creation
This commit is contained in:
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Circle, Download, Heart, Image as ImageIcon, Layout, ListTodo, Megaphone, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react';
|
||||
import { AlertCircle, Bell, CalendarDays, Camera, CheckCircle2, ChevronRight, Circle, Download, Heart, Image as ImageIcon, Layout, ListTodo, Megaphone, Plus, QrCode, Settings, ShieldCheck, Sparkles, TrendingUp, Tv, Users } from 'lucide-react';
|
||||
import { Button } from '@tamagui/button';
|
||||
import { Card } from '@tamagui/card';
|
||||
import { YGroup } from '@tamagui/group';
|
||||
@@ -19,7 +19,7 @@ import toast from 'react-hot-toast';
|
||||
import { MobileShell } from './components/MobileShell';
|
||||
import { ADMIN_EVENTS_PATH, adminPath } from '../constants';
|
||||
import { useEventContext } from '../context/EventContext';
|
||||
import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent, getLiveShowQueue } from '../api';
|
||||
import { getEventStats, EventStats, TenantEvent, getEventPhotos, TenantPhoto, updateEvent, getLiveShowQueue, getTenantPackagesOverview, type TenantPackageSummary } from '../api';
|
||||
import { formatEventDate } from '../lib/events';
|
||||
import { useAuth } from '../auth/context';
|
||||
import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme';
|
||||
@@ -37,6 +37,59 @@ function translateLimits(t: any) {
|
||||
return (key: string, options?: any) => t(`management:limits.${key}`, key, options);
|
||||
}
|
||||
|
||||
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 resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
|
||||
if (pkg.package_type !== 'reseller') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const remaining = toNumber(pkg.remaining_events);
|
||||
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;
|
||||
}
|
||||
|
||||
// --- TAMAGUI-ALIGNED PRIMITIVES ---
|
||||
|
||||
function DashboardCard({
|
||||
@@ -129,6 +182,7 @@ export default function MobileDashboardPage() {
|
||||
const { user } = useAuth();
|
||||
const theme = useAdminTheme();
|
||||
const isMember = user?.role === 'member';
|
||||
const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin';
|
||||
|
||||
// --- LOGIC ---
|
||||
const memberPermissions = React.useMemo(() => {
|
||||
@@ -178,6 +232,29 @@ export default function MobileDashboardPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: packagesOverview, isLoading: packagesLoading } = useQuery({
|
||||
queryKey: ['mobile', 'dashboard', 'packages-overview'],
|
||||
enabled: !isMember && !isSuperAdmin,
|
||||
queryFn: () => getTenantPackagesOverview({ force: true }),
|
||||
});
|
||||
|
||||
const canCreateEvent = React.useMemo(() => {
|
||||
if (!canManageEvents) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isMember || packagesLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activePackages = collectActivePackages(packagesOverview ?? null);
|
||||
return activePackages.some((pkg) => resellerHasRemainingEvents(pkg));
|
||||
}, [canManageEvents, isSuperAdmin, isMember, packagesLoading, packagesOverview]);
|
||||
|
||||
const locale = i18n.language?.startsWith('en') ? 'en-GB' : 'de-DE';
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -265,6 +342,7 @@ export default function MobileDashboardPage() {
|
||||
permissions={memberPermissions}
|
||||
isMember={isMember}
|
||||
isCompleted={isCompleted}
|
||||
canCreateEvent={canCreateEvent}
|
||||
/>
|
||||
|
||||
{/* 5. RECENT PHOTOS */}
|
||||
@@ -627,7 +705,7 @@ function PulseStrip({ event, stats, liveShowApprovedCount }: any) {
|
||||
return <KpiStrip items={items} />;
|
||||
}
|
||||
|
||||
function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }: any) {
|
||||
function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted, canCreateEvent }: any) {
|
||||
const theme = useAdminTheme();
|
||||
const { t } = useTranslation(['management', 'dashboard']);
|
||||
const slug = event?.slug;
|
||||
@@ -649,6 +727,7 @@ function UnifiedToolGrid({ event, navigate, permissions, isMember, isCompleted }
|
||||
].filter(Boolean) as ToolItem[];
|
||||
|
||||
const adminItems: ToolItem[] = [
|
||||
canCreateEvent ? { label: t('management:events.list.actions.create', 'Create Event'), icon: Plus, path: `/mobile/events/new`, color: theme.primary } : null,
|
||||
{ label: t('management:mobileDashboard.shortcutAnalytics', 'Analytics'), icon: TrendingUp, path: `/mobile/events/${slug}/analytics`, color: ADMIN_ACTION_COLORS.analytics },
|
||||
!isCompleted ? { label: t('events.recap.exportTitleShort', 'Exports'), icon: Download, path: `/mobile/exports`, color: ADMIN_ACTION_COLORS.recap } : null,
|
||||
{ label: t('management:mobileProfile.settings', 'Settings'), icon: Settings, path: `/mobile/events/${slug}/edit`, color: ADMIN_ACTION_COLORS.settings },
|
||||
|
||||
@@ -66,7 +66,7 @@ function allowPermission(permissions: string[], permission: string): boolean {
|
||||
|
||||
export default function MobileEventTasksPage() {
|
||||
const { slug: slugParam } = useParams<{ slug?: string }>();
|
||||
const { activeEvent, selectEvent } = useEventContext();
|
||||
const { activeEvent, selectEvent, refetch } = useEventContext();
|
||||
const slug = slugParam ?? activeEvent?.slug ?? null;
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('management');
|
||||
@@ -258,6 +258,7 @@ export default function MobileEventTasksPage() {
|
||||
const result = await getEventTasks(eventId, 1);
|
||||
setAssignedTasks(result.data);
|
||||
setLibrary((prev) => prev.filter((t) => t.id !== taskId));
|
||||
refetch();
|
||||
toast.success(t('events.tasks.assigned', 'Fotoaufgabe hinzugefügt'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -281,6 +282,7 @@ export default function MobileEventTasksPage() {
|
||||
const assignedIds = new Set(result.data.map((t) => t.id));
|
||||
setAssignedTasks(result.data);
|
||||
setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id)));
|
||||
refetch();
|
||||
toast.success(t('events.tasks.imported', 'Fotoaufgabenpaket importiert'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -337,6 +339,7 @@ export default function MobileEventTasksPage() {
|
||||
setLibrary((prev) => prev.filter((t) => !assignedIds.has(t.id)));
|
||||
setShowTaskSheet(false);
|
||||
setNewTask({ id: null, title: '', description: '', emotion_id: '', tenant_id: null });
|
||||
refetch();
|
||||
toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -361,6 +364,7 @@ export default function MobileEventTasksPage() {
|
||||
}
|
||||
return next;
|
||||
});
|
||||
refetch();
|
||||
toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -392,6 +396,7 @@ export default function MobileEventTasksPage() {
|
||||
setAssignedTasks((prev) => prev.filter((task) => !selectedTaskIds.has(task.id)));
|
||||
setSelectedTaskIds(new Set());
|
||||
setSelectionMode(false);
|
||||
refetch();
|
||||
toast.success(t('events.tasks.removed', 'Fotoaufgabe entfernt'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
@@ -519,6 +524,7 @@ export default function MobileEventTasksPage() {
|
||||
setAssignedTasks(result.data);
|
||||
setBulkLines('');
|
||||
setShowBulkSheet(false);
|
||||
refetch();
|
||||
toast.success(t('events.tasks.created', 'Fotoaufgabe gespeichert'));
|
||||
} catch (err) {
|
||||
if (!isAuthError(err)) {
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function MobileEventsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const isMember = user?.role === 'member';
|
||||
const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin';
|
||||
const [events, setEvents] = React.useState<TenantEvent[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
@@ -85,13 +86,21 @@ export default function MobileEventsPage() {
|
||||
}, [isMember]);
|
||||
|
||||
const canCreateEvent = React.useMemo(() => {
|
||||
if (isMember || packagesLoading) {
|
||||
if (isMember) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packagesLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activePackages = collectActivePackages(packagesOverview);
|
||||
return activePackages.some((pkg) => packageHasRemainingEvents(pkg));
|
||||
}, [isMember, packagesLoading, packagesOverview]);
|
||||
return activePackages.some((pkg) => resellerHasRemainingEvents(pkg));
|
||||
}, [isMember, isSuperAdmin, packagesLoading, packagesOverview]);
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
@@ -540,18 +549,13 @@ function toNumber(value: unknown): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function packageHasRemainingEvents(pkg: TenantPackageSummary): boolean {
|
||||
const remaining = toNumber(pkg.remaining_events);
|
||||
|
||||
function resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
|
||||
if (pkg.package_type !== 'reseller') {
|
||||
if (remaining !== null) {
|
||||
return remaining > 0;
|
||||
}
|
||||
|
||||
const usedEvents = toNumber(pkg.used_events) ?? 0;
|
||||
return usedEvents < 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
const remaining = toNumber(pkg.remaining_events);
|
||||
|
||||
if (remaining !== null) {
|
||||
return remaining > 0;
|
||||
}
|
||||
|
||||
@@ -399,7 +399,17 @@ describe('MobileDashboardPage', () => {
|
||||
|
||||
expect(screen.getByText('Experience')).toBeInTheDocument();
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Create Event')).not.toBeInTheDocument();
|
||||
|
||||
authState.user = { role: 'tenant_admin' };
|
||||
});
|
||||
|
||||
it('shows create event action for reseller packages', () => {
|
||||
fixtures.activePackage.package_type = 'reseller';
|
||||
fixtures.activePackage.remaining_events = 2;
|
||||
|
||||
render(<MobileDashboardPage />);
|
||||
|
||||
expect(screen.getByText('Create Event')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,11 +63,13 @@ vi.mock('../hooks/useBackNavigation', () => ({
|
||||
}));
|
||||
|
||||
const selectEventMock = vi.fn();
|
||||
const refetchMock = vi.fn();
|
||||
|
||||
vi.mock('../../context/EventContext', () => ({
|
||||
useEventContext: () => ({
|
||||
activeEvent: fixtures.event,
|
||||
selectEvent: selectEventMock,
|
||||
refetch: refetchMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -245,7 +247,12 @@ vi.mock('../components/FormControls', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../components/Sheet', () => ({
|
||||
MobileSheet: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
MobileSheet: ({ children, footer }: { children: React.ReactNode; footer?: React.ReactNode }) => (
|
||||
<div>
|
||||
{children}
|
||||
{footer}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../components/Tag', () => ({
|
||||
@@ -336,4 +343,23 @@ describe('MobileEventTasksPage', () => {
|
||||
expect(screen.queryByLabelText('Add')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('refetches events after creating a task', async () => {
|
||||
refetchMock.mockClear();
|
||||
(api.createTask as unknown as { mockResolvedValueOnce: (value: any) => void }).mockResolvedValueOnce({ id: 99 });
|
||||
|
||||
render(<MobileEventTasksPage />);
|
||||
|
||||
expect(await screen.findByText('Photo tasks for guests')).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('z.B. Erstes Gruppenfoto'), {
|
||||
target: { value: 'New Task' },
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Fotoaufgabe speichern' })[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(refetchMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -255,6 +255,41 @@ describe('MobileEventsPage', () => {
|
||||
});
|
||||
|
||||
it('shows the create button when an active package has remaining events', async () => {
|
||||
vi.mocked(api.getTenantPackagesOverview).mockResolvedValueOnce({
|
||||
packages: [
|
||||
{
|
||||
id: 2,
|
||||
package_id: 2,
|
||||
package_name: 'Reseller',
|
||||
package_type: 'reseller',
|
||||
included_package_slug: 'standard',
|
||||
active: true,
|
||||
used_events: 1,
|
||||
remaining_events: 2,
|
||||
price: 240,
|
||||
currency: 'EUR',
|
||||
purchased_at: null,
|
||||
expires_at: null,
|
||||
package_limits: { max_events_per_year: 5 },
|
||||
},
|
||||
],
|
||||
activePackage: {
|
||||
id: 2,
|
||||
package_id: 2,
|
||||
package_name: 'Reseller',
|
||||
package_type: 'reseller',
|
||||
included_package_slug: 'standard',
|
||||
active: true,
|
||||
used_events: 1,
|
||||
remaining_events: 2,
|
||||
price: 240,
|
||||
currency: 'EUR',
|
||||
purchased_at: null,
|
||||
expires_at: null,
|
||||
package_limits: { max_events_per_year: 5 },
|
||||
},
|
||||
});
|
||||
|
||||
render(<MobileEventsPage />);
|
||||
|
||||
expect(await screen.findByText('New event')).toBeInTheDocument();
|
||||
|
||||
@@ -16,11 +16,13 @@ export function EventSwitcherSheet({
|
||||
onClose,
|
||||
events,
|
||||
activeSlug,
|
||||
canCreateEvent,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
events: TenantEvent[];
|
||||
activeSlug: string | null;
|
||||
canCreateEvent: boolean;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation(['management', 'mobile']);
|
||||
const navigate = useNavigate();
|
||||
@@ -73,13 +75,15 @@ export function EventSwitcherSheet({
|
||||
);
|
||||
})}
|
||||
|
||||
<Pressable onPress={() => { onClose(); navigate(adminPath('/mobile/events/new')); }}>
|
||||
<XStack padding="$3" justifyContent="center">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.primary}>
|
||||
{t('mobile:header.createEvent', 'Create Event')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
{canCreateEvent ? (
|
||||
<Pressable onPress={() => { onClose(); navigate(adminPath('/mobile/events/new')); }}>
|
||||
<XStack padding="$3" justifyContent="center">
|
||||
<Text fontSize="$sm" fontWeight="700" color={theme.primary}>
|
||||
{t('mobile:header.createEvent', 'Create Event')}
|
||||
</Text>
|
||||
</XStack>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</YStack>
|
||||
</MobileSheet>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ChevronLeft, Bell, QrCode, ChevronsUpDown, Search } from 'lucide-react';
|
||||
import { YStack, XStack, SizableText as Text, Image } from 'tamagui';
|
||||
import { Pressable } from '@tamagui/react-native-web-lite';
|
||||
@@ -12,7 +13,7 @@ import { MobileCard, CTAButton } from './Primitives';
|
||||
import { useNotificationsBadge } from '../hooks/useNotificationsBadge';
|
||||
import { useOnlineStatus } from '../hooks/useOnlineStatus';
|
||||
import { resolveEventDisplayName } from '../../lib/events';
|
||||
import { TenantEvent, getEvents } from '../../api';
|
||||
import { TenantEvent, getEvents, getTenantPackagesOverview, type TenantPackageSummary } from '../../api';
|
||||
import { setTabHistory } from '../lib/tabHistory';
|
||||
import { loadPhotoQueue } from '../lib/photoModerationQueue';
|
||||
import { countQueuedPhotoActions } from '../lib/queueStatus';
|
||||
@@ -31,6 +32,59 @@ type MobileShellProps = {
|
||||
headerActions?: React.ReactNode;
|
||||
};
|
||||
|
||||
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 resellerHasRemainingEvents(pkg: TenantPackageSummary): boolean {
|
||||
if (pkg.package_type !== 'reseller') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const remaining = toNumber(pkg.remaining_events);
|
||||
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;
|
||||
}
|
||||
|
||||
export function MobileShell({ title, subtitle, children, activeTab, onBack, headerActions }: MobileShellProps) {
|
||||
const { events, activeEvent, selectEvent } = useEventContext();
|
||||
const { user } = useAuth();
|
||||
@@ -40,6 +94,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
const { t } = useTranslation('mobile');
|
||||
const { count: notificationCount } = useNotificationsBadge();
|
||||
const online = useOnlineStatus();
|
||||
const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin';
|
||||
|
||||
useDocumentTitle(title);
|
||||
|
||||
@@ -144,6 +199,29 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
};
|
||||
const showQr = Boolean(effectiveActive?.slug) && allowPermission('join-tokens:manage');
|
||||
|
||||
const { data: packagesOverview, isLoading: packagesLoading } = useQuery({
|
||||
queryKey: ['mobile', 'header', 'packages-overview'],
|
||||
enabled: !isMember && !isSuperAdmin,
|
||||
queryFn: () => getTenantPackagesOverview({ force: true }),
|
||||
});
|
||||
|
||||
const canCreateEvent = React.useMemo(() => {
|
||||
if (isMember) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packagesLoading) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activePackages = collectActivePackages(packagesOverview ?? null);
|
||||
return activePackages.some((pkg) => resellerHasRemainingEvents(pkg));
|
||||
}, [isMember, isSuperAdmin, packagesLoading, packagesOverview]);
|
||||
|
||||
// --- CONTEXT PILL ---
|
||||
const EventContextPill = () => {
|
||||
if (!effectiveActive || isEventsIndex || isCompactHeader) {
|
||||
@@ -387,10 +465,11 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
|
||||
<BottomNav active={activeTab} onNavigate={go} />
|
||||
|
||||
<EventSwitcherSheet
|
||||
open={switcherOpen}
|
||||
onClose={() => setSwitcherOpen(false)}
|
||||
events={effectiveEvents}
|
||||
activeSlug={effectiveActive?.slug ?? null}
|
||||
open={switcherOpen}
|
||||
onClose={() => setSwitcherOpen(false)}
|
||||
events={effectiveEvents}
|
||||
activeSlug={effectiveActive?.slug ?? null}
|
||||
canCreateEvent={canCreateEvent}
|
||||
/>
|
||||
<UserMenuSheet
|
||||
open={userMenuOpen}
|
||||
|
||||
@@ -112,6 +112,11 @@ vi.mock('../../hooks/useOnlineStatus', () => ({
|
||||
|
||||
vi.mock('../../../api', () => ({
|
||||
getEvents: vi.fn().mockResolvedValue([]),
|
||||
getTenantPackagesOverview: vi.fn().mockResolvedValue({ packages: [], activePackage: null }),
|
||||
}));
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: () => ({ data: { packages: [], activePackage: null }, isLoading: false }),
|
||||
}));
|
||||
|
||||
vi.mock('../../lib/tabHistory', () => ({
|
||||
|
||||
@@ -14,8 +14,7 @@ import React from 'react';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { useEffect } from 'react';
|
||||
import { Sentry, initSentry } from './lib/sentry';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
import { buildPageTitle } from './lib/appTitle';
|
||||
const InertiaFallback: React.FC = () => (
|
||||
<div className="flex min-h-screen items-center justify-center text-sm text-muted-foreground">
|
||||
Oberfläche konnte nicht geladen werden.
|
||||
@@ -48,7 +47,7 @@ const LocaleSync: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
};
|
||||
|
||||
createInertiaApp({
|
||||
title: (title) => title ? `${title} - ${appName}` : appName,
|
||||
title: (title) => buildPageTitle(title),
|
||||
resolve: (name) =>
|
||||
resolvePageComponent(
|
||||
`./pages/${name}.tsx`,
|
||||
|
||||
28
resources/js/lib/__tests__/appTitle.test.ts
Normal file
28
resources/js/lib/__tests__/appTitle.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPageTitle, getAppName } from '../appTitle';
|
||||
|
||||
describe('app title helpers', () => {
|
||||
it('falls back to Fotospiel when VITE_APP_NAME is missing', () => {
|
||||
const original = import.meta.env.VITE_APP_NAME;
|
||||
// @ts-expect-error - import.meta.env is mutable in vitest
|
||||
import.meta.env.VITE_APP_NAME = '';
|
||||
|
||||
expect(getAppName()).toBe('Fotospiel');
|
||||
expect(buildPageTitle('Demo')).toBe('Demo - Fotospiel');
|
||||
|
||||
// @ts-expect-error - restore original value
|
||||
import.meta.env.VITE_APP_NAME = original;
|
||||
});
|
||||
|
||||
it('uses the configured VITE_APP_NAME when available', () => {
|
||||
const original = import.meta.env.VITE_APP_NAME;
|
||||
// @ts-expect-error - import.meta.env is mutable in vitest
|
||||
import.meta.env.VITE_APP_NAME = 'Fotospiel App';
|
||||
|
||||
expect(getAppName()).toBe('Fotospiel App');
|
||||
expect(buildPageTitle('Demo')).toBe('Demo - Fotospiel App');
|
||||
|
||||
// @ts-expect-error - restore original value
|
||||
import.meta.env.VITE_APP_NAME = original;
|
||||
});
|
||||
});
|
||||
8
resources/js/lib/appTitle.ts
Normal file
8
resources/js/lib/appTitle.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function getAppName(): string {
|
||||
return import.meta.env.VITE_APP_NAME || 'Fotospiel';
|
||||
}
|
||||
|
||||
export function buildPageTitle(title?: string): string {
|
||||
const appName = getAppName();
|
||||
return title ? `${title} - ${appName}` : appName;
|
||||
}
|
||||
@@ -4,14 +4,13 @@ import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from './i18n';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
import { buildPageTitle } from './lib/appTitle';
|
||||
|
||||
createServer((page) =>
|
||||
createInertiaApp({
|
||||
page,
|
||||
render: ReactDOMServer.renderToString,
|
||||
title: (title) => (title ? `${title} - ${appName}` : appName),
|
||||
title: (title) => buildPageTitle(title),
|
||||
resolve: (name) => resolvePageComponent(`./pages/${name}.tsx`, import.meta.glob('./pages/**/*.tsx')),
|
||||
setup: ({ App, props }) => {
|
||||
const locale = (props.initialPage?.props as Record<string, unknown> | undefined)?.locale as string | undefined;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||
<title inertia>{{ config('app.name', 'Fotospiel') }}</title>
|
||||
|
||||
<link rel="icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
Reference in New Issue
Block a user