Files
thetool/public/mobile/app.js
2026-01-18 13:25:14 +01:00

621 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { authState, checkAuth, login, logout } from '/mobile/shared/auth.js';
import LoginScreen from '/mobile/components/LoginScreen.js';
import MainMenu from '/mobile/components/MainMenu.js';
import LagerModule from '/mobile/modules/lager/LagerModule.js';
import ShippingNoteModule from '/mobile/modules/lager/shippingnote/ShippingNoteModule.js';
import WorkorderModule from '/mobile/modules/workorder/WorkorderModule.js';
const { createApp, ref, computed, onMounted, onUnmounted, watch } = Vue;
const isPWAInstalled = () => {
if (window.matchMedia('(display-mode: standalone)').matches) return true;
if (window.navigator.standalone === true) return true;
if (document.referrer.includes('android-app://')) return true;
return false;
};
const shouldRequirePWA = () => {
return window.location.hostname === 'thetool.xinon.at';
};
const parseInitialRoute = () => {
const initialPath = window.TT_CONFIG?.INITIAL_PATH || '/MobileApp';
const parts = initialPath.replace('/MobileApp', '').split('/').filter(Boolean);
return {
module: parts[0] || null,
submodule: parts[1] || null
};
};
const App = {
components: {
LoginScreen,
MainMenu,
LagerModule,
ShippingNoteModule,
WorkorderModule
},
setup() {
const currentView = ref('loading');
const user = ref(null);
const toast = ref({ show: false, message: '', type: 'success' });
const theme = ref('system');
const showSettings = ref(false);
const lagerSimpleMode = ref(false);
const currentModule = ref(null);
const currentSubmodule = ref(null);
const lastWorkflow = ref(null);
const showContinuePrompt = ref(false);
const showInstallPrompt = ref(false);
const deferredInstallPrompt = ref(null);
const isIOS = ref(/iPad|iPhone|iPod/.test(navigator.userAgent));
const isAndroid = ref(/Android/.test(navigator.userAgent));
const canGoBack = computed(() => currentModule.value !== null || workorderDetailOpen.value);
const workorderDetailOpen = ref(false);
const workorderRef = ref(null);
const applyTheme = () => {
const isDark = localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
if (metaThemeColor) {
metaThemeColor.setAttribute('content', isDark ? '#0f172a' : '#005384');
}
};
const setTheme = (newTheme) => {
theme.value = newTheme;
if (newTheme === 'system') {
localStorage.removeItem('theme');
} else {
localStorage.setItem('theme', newTheme);
}
applyTheme();
};
const handleInstallPrompt = (e) => {
e.preventDefault();
deferredInstallPrompt.value = e;
};
const triggerInstall = async () => {
if (!deferredInstallPrompt.value) return;
deferredInstallPrompt.value.prompt();
const { outcome } = await deferredInstallPrompt.value.userChoice;
if (outcome === 'accepted') {
showInstallPrompt.value = false;
window.location.reload();
}
deferredInstallPrompt.value = null;
};
const loadLagerSettings = () => {
try {
const saved = localStorage.getItem('movement_settings');
if (saved) {
const settings = JSON.parse(saved);
lagerSimpleMode.value = settings.simpleMode || false;
}
} catch (e) {}
};
const setLagerSimpleMode = (value) => {
lagerSimpleMode.value = value;
try {
const saved = localStorage.getItem('movement_settings');
const settings = saved ? JSON.parse(saved) : {};
settings.simpleMode = value;
localStorage.setItem('movement_settings', JSON.stringify(settings));
} catch (e) {}
};
const saveLastWorkflow = (module, submodule) => {
if (module) {
const workflow = { module, submodule: submodule || null, timestamp: Date.now() };
localStorage.setItem('lastWorkflow', JSON.stringify(workflow));
lastWorkflow.value = workflow;
}
};
const loadLastWorkflow = () => {
try {
const saved = localStorage.getItem('lastWorkflow');
if (saved) {
const workflow = JSON.parse(saved);
if (Date.now() - workflow.timestamp < 24 * 60 * 60 * 1000) {
return workflow;
}
}
} catch (e) {}
return null;
};
const navigate = (module, submodule = null) => {
currentModule.value = module;
currentSubmodule.value = submodule;
showContinuePrompt.value = false;
saveLastWorkflow(module, submodule);
let path = '/MobileApp';
if (module) path += '/' + module;
if (submodule) path += '/' + submodule;
history.pushState({ module, submodule }, '', path);
};
const continueLastWorkflow = () => {
if (lastWorkflow.value) {
navigate(lastWorkflow.value.module, lastWorkflow.value.submodule);
}
};
const dismissContinuePrompt = () => {
showContinuePrompt.value = false;
};
const goHome = () => {
navigate(null, null);
};
const goBack = () => {
if (workorderDetailOpen.value && workorderRef.value?.closeDetail) {
workorderRef.value.closeDetail();
return;
}
if (currentSubmodule.value) {
navigate(currentModule.value, null);
} else if (currentModule.value) {
navigate(null, null);
}
};
const handleWorkorderDetailOpen = (workorderId) => {
workorderDetailOpen.value = true;
};
const handleWorkorderDetailClose = () => {
workorderDetailOpen.value = false;
};
const handlePopstate = (event) => {
if (workorderDetailOpen.value && workorderRef.value?.closeDetail) {
workorderRef.value.closeDetail();
return;
}
if (event.state) {
currentModule.value = event.state.module;
currentSubmodule.value = event.state.submodule;
} else {
currentModule.value = null;
currentSubmodule.value = null;
}
};
const handleLogin = async (credentials) => {
if (credentials._2faSuccess) {
user.value = credentials.user;
currentView.value = 'app';
showToast('Erfolgreich angemeldet', 'success');
return { success: true };
}
const result = await login(credentials);
if (result.success) {
user.value = result.user;
currentView.value = 'app';
showToast('Erfolgreich angemeldet', 'success');
}
return result;
};
const handleLogout = async () => {
await logout();
user.value = null;
currentModule.value = null;
currentSubmodule.value = null;
currentView.value = 'login';
showToast('Abgemeldet', 'success');
};
const showToast = (message, type = 'success') => {
toast.value = { show: true, message, type };
setTimeout(() => { toast.value.show = false; }, 3000);
};
const currentComponent = computed(() => {
if (currentView.value !== 'app') return null;
if (!currentModule.value) return 'MainMenu';
if (currentModule.value.toLowerCase() === 'lieferschein') return 'ShippingNoteModule';
if (currentModule.value.toLowerCase() === 'lager') return 'LagerModule';
if (currentModule.value.toLowerCase() === 'workorder') return 'WorkorderModule';
return 'MainMenu';
});
const breadcrumbs = computed(() => {
const crumbs = [{ label: 'Home', module: null, submodule: null }];
if (currentModule.value) {
crumbs.push({ label: currentModule.value, module: currentModule.value, submodule: null });
}
if (currentSubmodule.value) {
crumbs.push({ label: currentSubmodule.value, module: currentModule.value, submodule: currentSubmodule.value });
}
return crumbs;
});
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
onMounted(async () => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) theme.value = savedTheme;
applyTheme();
mediaQuery.addEventListener('change', applyTheme);
window.addEventListener('popstate', handlePopstate);
window.addEventListener('beforeinstallprompt', handleInstallPrompt);
loadLagerSettings();
if (shouldRequirePWA() && !isPWAInstalled()) {
showInstallPrompt.value = true;
currentView.value = 'install';
return;
}
const result = await checkAuth();
if (result.authenticated) {
user.value = result.user;
currentView.value = 'app';
const initialRoute = parseInitialRoute();
currentModule.value = initialRoute.module;
currentSubmodule.value = initialRoute.submodule;
history.replaceState(
{ module: initialRoute.module, submodule: initialRoute.submodule },
'',
window.location.pathname
);
if (!initialRoute.module && !initialRoute.submodule) {
const saved = loadLastWorkflow();
if (saved) {
lastWorkflow.value = saved;
showContinuePrompt.value = true;
}
}
} else {
currentView.value = 'login';
}
});
onUnmounted(() => {
mediaQuery.removeEventListener('change', applyTheme);
window.removeEventListener('popstate', handlePopstate);
window.removeEventListener('beforeinstallprompt', handleInstallPrompt);
});
return {
currentView,
user,
toast,
theme,
showSettings,
currentModule,
currentSubmodule,
currentComponent,
canGoBack,
breadcrumbs,
handleLogin,
handleLogout,
navigate,
goHome,
goBack,
showToast,
setTheme,
lagerSimpleMode,
setLagerSimpleMode,
showInstallPrompt,
deferredInstallPrompt,
isIOS,
isAndroid,
triggerInstall,
lastWorkflow,
showContinuePrompt,
continueLastWorkflow,
dismissContinuePrompt,
workorderRef,
handleWorkorderDetailOpen,
handleWorkorderDetailClose,
};
},
template: `
<div class="relative h-full w-full bg-slate-50 dark:bg-slate-900 transition-colors duration-300">
<div v-if="currentView === 'loading'" class="flex items-center justify-center h-full">
<div class="text-center">
<img src="/assets/images/xinon-full-transparent.png" class="h-12 mx-auto mb-4 dark:hidden">
<img src="/assets/images/xinon-full-transparent-white.png" class="h-12 mx-auto mb-4 hidden dark:block">
<div class="animate-pulse text-slate-500 dark:text-slate-400">Lädt...</div>
</div>
</div>
<div v-else-if="currentView === 'install'" class="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
<div class="absolute inset-0 opacity-40" style="background-image: linear-gradient(to right, rgba(0, 83, 132, 0.15) 1px, transparent 1px), linear-gradient(to bottom, rgba(0, 83, 132, 0.15) 1px, transparent 1px); background-size: 50px 50px;"></div>
<div class="absolute w-3 h-3 bg-cyan-400 rounded-full network-node" style="top: 12%; left: 18%; box-shadow: 0 0 25px 8px rgba(34, 211, 238, 0.6);"></div>
<div class="absolute w-2.5 h-2.5 bg-blue-400 rounded-full network-node" style="top: 22%; left: 78%; animation-delay: 0.5s; box-shadow: 0 0 20px 6px rgba(96, 165, 250, 0.6);"></div>
<div class="absolute w-3 h-3 bg-cyan-300 rounded-full network-node-slow" style="top: 72%; left: 12%; animation-delay: 1s; box-shadow: 0 0 25px 8px rgba(103, 232, 249, 0.6);"></div>
<div class="absolute w-2.5 h-2.5 bg-blue-300 rounded-full network-node" style="top: 82%; left: 85%; animation-delay: 0.3s; box-shadow: 0 0 20px 6px rgba(147, 197, 253, 0.6);"></div>
</div>
<div class="relative bg-white/95 dark:bg-slate-800/95 backdrop-blur-sm rounded-2xl shadow-2xl p-6 w-full max-w-sm border border-white/20 dark:border-slate-700/50">
<div class="mb-6">
<img src="/assets/images/xinon-full-transparent.png" class="h-14 mx-auto dark:hidden" alt="Logo">
<img src="/assets/images/xinon-full-transparent-white.png" class="h-14 mx-auto hidden dark:block" alt="Logo">
</div>
<div class="text-center mb-6">
<div class="w-16 h-16 bg-primary/10 rounded-2xl flex items-center justify-center mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<h2 class="text-xl font-bold text-slate-800 dark:text-white mb-2">App installieren</h2>
<p class="text-sm text-slate-500 dark:text-slate-400">
Für die beste Erfahrung installiere die App auf deinem Gerät.
</p>
</div>
<div v-if="isAndroid && deferredInstallPrompt">
<button
@click="triggerInstall"
class="w-full py-3 px-4 bg-primary text-white font-bold rounded-xl hover:bg-primary/90 focus:ring-4 focus:ring-primary/30 transition flex items-center justify-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
App installieren
</button>
</div>
<div v-else-if="isIOS" class="space-y-4">
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-xl p-4">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">So installierst du die App:</p>
<ol class="text-sm text-slate-600 dark:text-slate-400 space-y-3">
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 text-primary rounded-full flex items-center justify-center text-xs font-bold">1</span>
<span>Tippe auf das <strong>Teilen</strong>-Symbol
<svg xmlns="http://www.w3.org/2000/svg" class="inline h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
</span>
</li>
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 text-primary rounded-full flex items-center justify-center text-xs font-bold">2</span>
<span>Scrolle und wähle <strong>"Zum Home-Bildschirm"</strong></span>
</li>
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 text-primary rounded-full flex items-center justify-center text-xs font-bold">3</span>
<span>Tippe auf <strong>"Hinzufügen"</strong></span>
</li>
</ol>
</div>
</div>
<div v-else-if="isAndroid" class="space-y-4">
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-xl p-4">
<p class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">So installierst du die App:</p>
<ol class="text-sm text-slate-600 dark:text-slate-400 space-y-3">
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 text-primary rounded-full flex items-center justify-center text-xs font-bold">1</span>
<span>Tippe auf das <strong>Menü</strong> (⋮) oben rechts</span>
</li>
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 text-primary rounded-full flex items-center justify-center text-xs font-bold">2</span>
<span>Wähle <strong>"App installieren"</strong> oder <strong>"Zum Startbildschirm hinzufügen"</strong></span>
</li>
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 text-primary rounded-full flex items-center justify-center text-xs font-bold">3</span>
<span>Bestätige mit <strong>"Installieren"</strong></span>
</li>
</ol>
</div>
</div>
<div v-else class="space-y-4">
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4">
<p class="text-sm text-amber-700 dark:text-amber-400">
<strong>Hinweis:</strong> Diese App ist für mobile Geräte optimiert. Bitte öffne diese Seite auf deinem Smartphone und installiere die App.
</p>
</div>
</div>
<div class="mt-4 pt-4 border-t border-slate-100 dark:border-slate-700 text-center">
<p class="text-xs text-slate-400 dark:text-slate-500">
powered by <span class="font-semibold">XINON</span>
</p>
</div>
</div>
</div>
<LoginScreen
v-else-if="currentView === 'login'"
@login="handleLogin"
:theme="theme"
@set-theme="setTheme"
/>
<template v-else-if="currentView === 'app'">
<div class="h-full flex flex-col">
<header class="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-2 flex items-center app-header flex-shrink-0 z-10">
<button
@click="goBack"
:class="[
'w-10 h-10 flex items-center justify-center rounded-full transition',
canGoBack
? 'text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'
: 'text-transparent pointer-events-none'
]"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div class="flex-1 flex justify-center">
<img src="/assets/images/xinon-full-transparent.png" class="h-7 dark:hidden" alt="Logo">
<img src="/assets/images/xinon-full-transparent-white.png" class="h-7 hidden dark:block" alt="Logo">
</div>
<button
@click="showSettings = true"
class="w-10 h-10 flex items-center justify-center rounded-full text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</header>
<main class="flex-1 overflow-y-auto">
<div v-if="showContinuePrompt && !currentModule" class="p-3 pb-0">
<div class="bg-primary/10 dark:bg-primary/20 border border-primary/30 rounded-xl p-4">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
<p class="text-xs text-primary/70 dark:text-primary/80 font-medium uppercase tracking-wide">Fortsetzen</p>
<p class="font-semibold text-slate-800 dark:text-white truncate">
{{ lastWorkflow?.module }}<template v-if="lastWorkflow?.submodule"> {{ lastWorkflow.submodule }}</template>
</p>
</div>
<div class="flex gap-2 ml-3">
<button @click="dismissContinuePrompt" class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<button @click="continueLastWorkflow" class="px-4 py-2 bg-primary text-white font-bold rounded-lg hover:bg-primary/90 transition">
Weiter
</button>
</div>
</div>
</div>
</div>
<MainMenu
v-if="!currentModule"
:user="user"
@navigate="navigate"
/>
<LagerModule
v-else-if="currentModule?.toLowerCase() === 'lager'"
:user="user"
:submodule="currentSubmodule"
:simple-mode="lagerSimpleMode"
@navigate="navigate"
@toast="showToast"
/>
<ShippingNoteModule
v-else-if="currentModule?.toLowerCase() === 'lieferschein'"
:user="user"
@toast="showToast"
/>
<WorkorderModule
v-else-if="currentModule?.toLowerCase() === 'workorder'"
ref="workorderRef"
:user="user"
@navigate="navigate"
@toast="showToast"
@detail-open="handleWorkorderDetailOpen"
@detail-close="handleWorkorderDetailClose"
/>
</main>
</div>
<transition name="slide-right">
<div v-if="showSettings" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/40" @click="showSettings = false"></div>
<div class="absolute right-0 top-0 bottom-0 w-72 bg-white dark:bg-slate-800 shadow-xl flex flex-col">
<div class="safe-area-top border-b border-slate-200 dark:border-slate-700 px-4 py-3 flex items-center justify-between">
<h2 class="font-semibold text-slate-800 dark:text-white">Einstellungen</h2>
<button @click="showSettings = false" class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-full text-slate-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto">
<div class="px-4 py-3 border-b border-slate-100 dark:border-slate-700">
<p class="font-medium text-slate-800 dark:text-white">{{ user?.name }}</p>
<p class="text-sm text-slate-500 dark:text-slate-400">{{ user?.username }}</p>
</div>
<div class="px-4 py-3">
<p class="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-2">Farbschema</p>
<div class="flex space-x-2">
<button
@click="setTheme('light')"
:class="[theme === 'light' ? 'ring-2 ring-primary bg-primary/5' : 'bg-slate-100 dark:bg-slate-700', 'flex-1 py-2 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-300']"
>Hell</button>
<button
@click="setTheme('dark')"
:class="[theme === 'dark' ? 'ring-2 ring-primary bg-primary/5' : 'bg-slate-100 dark:bg-slate-700', 'flex-1 py-2 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-300']"
>Dunkel</button>
<button
@click="setTheme('system')"
:class="[theme === 'system' ? 'ring-2 ring-primary bg-primary/5' : 'bg-slate-100 dark:bg-slate-700', 'flex-1 py-2 rounded-lg text-sm font-medium text-slate-700 dark:text-slate-300']"
>Auto</button>
</div>
</div>
<div class="px-4 py-3 border-t border-slate-100 dark:border-slate-700">
<p class="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-3">Lager</p>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-slate-800 dark:text-white text-sm">Simpel Modus</p>
<p class="text-xs text-slate-500 dark:text-slate-400">Weniger Optionen</p>
</div>
<button
@click="setLagerSimpleMode(!lagerSimpleMode)"
:class="[
'relative w-11 h-6 rounded-full transition-colors',
lagerSimpleMode ? 'bg-green-500' : 'bg-slate-300 dark:bg-slate-600'
]"
>
<span :class="[
'absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform',
lagerSimpleMode ? 'left-5' : 'left-0.5'
]"></span>
</button>
</div>
</div>
</div>
<div class="p-3 border-t border-slate-100 dark:border-slate-700">
<button
@click="showSettings = false; handleLogout()"
class="w-full py-2.5 px-4 text-red-600 dark:text-red-400 font-medium rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition flex items-center justify-center"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Abmelden
</button>
</div>
</div>
</div>
</transition>
</template>
<transition name="slide-up">
<div v-if="toast.show" class="toast-container">
<div :class="['toast', 'toast-' + toast.type]">
{{ toast.message }}
</div>
</div>
</transition>
</div>
`
};
createApp(App).mount('#app');