resources/js/admin/mobile/lib.
- Admin push is end‑to‑end: new backend model/migration/service/job + API endpoints, admin runtime config, push‑aware
service worker, and a settings toggle via useAdminPushSubscription. Notifications now auto‑refresh on push.
- New PHP/JS tests: admin push API feature test and queue/haptics unit tests
Added admin-specific PWA icon assets and wired them into the admin manifest, service worker, and admin shell, plus a
new “Device & permissions” card in mobile Settings with a persistent storage action and translations.
Details: public/manifest.json, public/admin-sw.js, resources/views/admin.blade.php, new icons in public/; new hook
resources/js/admin/mobile/hooks/useDevicePermissions.ts, helpers/tests in resources/js/admin/mobile/lib/
devicePermissions.ts + resources/js/admin/mobile/lib/devicePermissions.test.ts, and Settings UI updates in resources/
js/admin/mobile/SettingsPage.tsx with copy in resources/js/admin/i18n/locales/en/management.json and resources/js/
admin/i18n/locales/de/management.json.
222 lines
6.2 KiB
TypeScript
222 lines
6.2 KiB
TypeScript
import React from 'react';
|
||
import { Loader2, PanelLeftClose, PanelRightOpen } from 'lucide-react';
|
||
import { XStack, YStack } from '@tamagui/stacks';
|
||
import { SizableText as Text } from '@tamagui/text';
|
||
import { Button } from '@tamagui/button';
|
||
import { useTheme } from '@tamagui/core';
|
||
|
||
const DEV_TENANT_KEYS = [
|
||
{ key: 'cust-standard-empty', label: 'Endkunde – Standard (kein Event)' },
|
||
{ key: 'cust-starter-wedding', label: 'Endkunde – Starter (Hochzeit)' },
|
||
{ key: 'reseller-s-active', label: 'Reseller S – 3 aktive Events' },
|
||
{ key: 'reseller-s-full', label: 'Reseller S – voll belegt (5/5)' },
|
||
] as const;
|
||
|
||
declare global {
|
||
interface Window {
|
||
fotospielDemoAuth?: {
|
||
clients: Record<string, string>;
|
||
loginAs: (tenantKey: string) => Promise<void>;
|
||
};
|
||
}
|
||
}
|
||
|
||
type DevTenantSwitcherProps = {
|
||
bottomOffset?: number;
|
||
variant?: 'floating' | 'inline';
|
||
};
|
||
|
||
export function DevTenantSwitcher({ bottomOffset = 16, variant = 'floating' }: DevTenantSwitcherProps) {
|
||
const helper = window.fotospielDemoAuth;
|
||
const theme = useTheme();
|
||
const [loggingIn, setLoggingIn] = React.useState<string | null>(null);
|
||
const [collapsed, setCollapsed] = React.useState<boolean>(() => {
|
||
if (typeof window === 'undefined') {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
return window.localStorage.getItem('fotospiel-dev-switcher-collapsed') === '1';
|
||
} catch (error) {
|
||
console.warn('[DevAuth] Failed to read collapse state', error);
|
||
return false;
|
||
}
|
||
});
|
||
|
||
React.useEffect(() => {
|
||
if (typeof window === 'undefined') {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
window.localStorage.setItem('fotospiel-dev-switcher-collapsed', collapsed ? '1' : '0');
|
||
} catch (error) {
|
||
console.warn('[DevAuth] Failed to persist collapse state', error);
|
||
}
|
||
}, [collapsed]);
|
||
|
||
if (!helper) {
|
||
return null;
|
||
}
|
||
|
||
async function handleLogin(key: string) {
|
||
if (!helper) {
|
||
return;
|
||
}
|
||
setLoggingIn(key);
|
||
try {
|
||
await helper.loginAs(key);
|
||
} catch (error) {
|
||
console.error('[DevAuth] Switch failed', error);
|
||
setLoggingIn(null);
|
||
}
|
||
}
|
||
|
||
if (variant === 'inline') {
|
||
if (collapsed) {
|
||
return (
|
||
<Button
|
||
size="$2"
|
||
theme="yellow"
|
||
onPress={() => setCollapsed(false)}
|
||
borderRadius={999}
|
||
icon={<PanelRightOpen size={16} />}
|
||
>
|
||
Demo tenants
|
||
</Button>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<YStack
|
||
borderWidth={1}
|
||
borderColor="rgba(234,179,8,0.5)"
|
||
backgroundColor="rgba(255,255,255,0.95)"
|
||
padding="$3"
|
||
space="$2"
|
||
borderRadius="$4"
|
||
shadowColor="#f59e0b"
|
||
shadowOpacity={0.25}
|
||
shadowRadius={14}
|
||
shadowOffset={{ width: 0, height: 8 }}
|
||
maxWidth={320}
|
||
>
|
||
<XStack alignItems="center" justifyContent="space-between">
|
||
<XStack alignItems="center" space="$2">
|
||
<Text fontSize={13} fontWeight="800" color="#92400e">
|
||
Demo tenants
|
||
</Text>
|
||
<Text fontSize={10} color="#a16207" textTransform="uppercase" letterSpacing={1}>
|
||
Dev mode
|
||
</Text>
|
||
</XStack>
|
||
<Button
|
||
size="$2"
|
||
theme="yellow"
|
||
circular
|
||
icon={<PanelLeftClose size={14} />}
|
||
onPress={() => setCollapsed(true)}
|
||
aria-label="Switcher minimieren"
|
||
/>
|
||
</XStack>
|
||
<YStack space="$1">
|
||
{DEV_TENANT_KEYS.map(({ key, label }) => (
|
||
<Button
|
||
key={key}
|
||
size="$3"
|
||
variant="outlined"
|
||
theme="yellow"
|
||
disabled={Boolean(loggingIn)}
|
||
onPress={() => void handleLogin(key)}
|
||
icon={loggingIn === key ? <Loader2 size={14} className="animate-spin" /> : undefined}
|
||
>
|
||
{loggingIn === key ? 'Verbinde...' : label}
|
||
</Button>
|
||
))}
|
||
</YStack>
|
||
</YStack>
|
||
);
|
||
}
|
||
|
||
if (collapsed) {
|
||
return (
|
||
<Button
|
||
size="$3"
|
||
theme="yellow"
|
||
icon={<PanelRightOpen size={16} />}
|
||
borderRadius={999}
|
||
position="fixed"
|
||
right="$4"
|
||
zIndex={1000}
|
||
onPress={() => setCollapsed(false)}
|
||
style={{ bottom: bottomOffset + 70 }}
|
||
>
|
||
Demo tenants
|
||
</Button>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<YStack
|
||
position="fixed"
|
||
right="$4"
|
||
zIndex={1000}
|
||
maxWidth={320}
|
||
space="$2"
|
||
borderWidth={1}
|
||
borderColor="rgba(234,179,8,0.5)"
|
||
backgroundColor="rgba(255,255,255,0.95)"
|
||
padding="$3"
|
||
borderRadius="$4"
|
||
shadowColor="#f59e0b"
|
||
shadowOpacity={0.25}
|
||
shadowRadius={14}
|
||
shadowOffset={{ width: 0, height: 8 }}
|
||
pointerEvents="auto"
|
||
style={{ bottom: bottomOffset + 70 }}
|
||
>
|
||
<XStack alignItems="center" justifyContent="space-between">
|
||
<XStack alignItems="center" space="$2">
|
||
<Text fontSize={13} fontWeight="800" color="#92400e">
|
||
Demo tenants
|
||
</Text>
|
||
<Text fontSize={10} color="#a16207" textTransform="uppercase" letterSpacing={1}>
|
||
Dev mode
|
||
</Text>
|
||
</XStack>
|
||
<Button
|
||
size="$2"
|
||
theme="yellow"
|
||
circular
|
||
icon={<PanelLeftClose size={14} />}
|
||
onPress={() => setCollapsed(true)}
|
||
aria-label="Switcher minimieren"
|
||
/>
|
||
</XStack>
|
||
<Text fontSize={11} color="#a16207">
|
||
Select a seeded tenant to mint Sanctum PATs and jump straight into their admin space. Available only in development builds.
|
||
</Text>
|
||
<YStack space="$1">
|
||
{DEV_TENANT_KEYS.map(({ key, label }) => (
|
||
<Button
|
||
key={key}
|
||
size="$3"
|
||
variant="outlined"
|
||
theme="yellow"
|
||
disabled={Boolean(loggingIn)}
|
||
onPress={() => void handleLogin(key)}
|
||
icon={loggingIn === key ? <Loader2 size={14} className="animate-spin" /> : undefined}
|
||
>
|
||
{loggingIn === key ? 'Verbinde...' : label}
|
||
</Button>
|
||
))}
|
||
</YStack>
|
||
<Text fontSize={10} color="#a16207">
|
||
Console: <Text as="span" fontFamily="$mono">fotospielDemoAuth.loginAs('lumen')</Text>
|
||
</Text>
|
||
</YStack>
|
||
);
|
||
}
|
||
|
||
export default DevTenantSwitcher;
|